Learn how to quickly integrate rich Generative AI experiences directly into Stream Chat. Learn More ->

Build a Swift Chat Messaging App with UIKit

Let's have a look at how to get started with the Swift Chat SDK by building an example messaging app.

By going through our tutorial you will learn everything you need to get started with our Swift SDK to build an iOS chat app. Once we are done, you will have created a chat application that is fully functional and ready for you to build upon.

example of swift ios chat sdk

Creating a project

The completed app for each step of the tutorial is available on GitHub.

To get started with the iOS Chat SDK, open Xcode and create a new project. In this tutorial we are going to create a chat app using UIKit. If you prefer using SwiftUI make sure to look at our SwiftUI tutorial.

  • Create a new Xcode project in Xcode (14 or later)
  • Choose iOS from the list of platforms
  • Choose the App template
  • Use "ChatDemo" for the product name
  • Select Storyboard in the Interface options
  • Make sure Swift is selected as the language option and press "Next" button.

  • Select where you want to store your new project and press "Create" button. Your are now ready to start with the tutorial.

In this tutorial we are going to use Swift Package Manager as the dependency manager. The SDK can also be installed with CocoaPods, you can find more information about that on the SDK doc pages.

  • Go to File > Add Packages...
  • Paste the following URL in the search field at the top right: https://github.com/getstream/stream-chat-swift
  • Under Dependency Rule go with the "Up to Next Major Version" option and enter 4.0.0 as the version
  • Click the Add Package button
  • Add both StreamChat and StreamChatUI packages to the project

Displaying a List of Channels

Stream provides a low-level client, offline support, and convenient UI components to help you quickly build your messaging interface. In this section, we'll be using the UI components to quickly display a channel list. The first thing which we will do is to create a globally accessible shared instance of the Stream Chat Client and initialize its connection when the app starts. Next, we will fetch the channels which our user participates in and display it.

First, open the AppDelegate.swift file and add this extension to ChatClient at the top of the file:

swift
1
2
3
4
5
import StreamChat extension ChatClient { static var shared: ChatClient! }

Next, open ViewController.swift and change its content to this:

swift
1
2
3
4
5
import StreamChat import StreamChatUI import UIKit class DemoChannelList: ChatChannelListVC {}

Next, open up SceneDelegate.swift add import StreamChat at the top of the file and change the scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) method to the following:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { let config = ChatClientConfig(apiKey: .init("dz5f4d5kzrue")) /// user id and token for the user let userId = "tutorial-droid" let token: Token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidHV0b3JpYWwtZHJvaWQifQ.NhEr0hP9W9nwqV7ZkdShxvi02C5PR7SJE7Cs4y7kyqg" /// Step 1: create an instance of ChatClient and share it using the singleton ChatClient.shared = ChatClient(config: config) /// Step 2: connect to chat ChatClient.shared.connectUser( userInfo: UserInfo( id: userId, name: "Tutorial Droid", imageURL: URL(string: "https://bit.ly/2TIt8NR") ), token: token ) /// Step 3: create the ChannelList view controller let channelList = DemoChannelList() let query = ChannelListQuery(filter: .containMembers(userIds: [userId])) channelList.controller = ChatClient.shared.channelListController(query: query) /// Step 4: similar to embedding with a navigation controller using Storyboard window?.rootViewController = UINavigationController(rootViewController: channelList) }

Run the application and you will be able to see a built-in list of channels for the tutorial user, open channels from the list as well and try all built-in chat features.

Let's have a quick look at the different steps in source code shown above:

  1. We initialize the shared ChatClient using an API key. This API key points to a tutorial environment, but you can sign up for a free Chat trial to get your own later.
  2. We create and connect the user using ChatClient.connectUser method and use a pre-generated user token, in order to authenticate the user. In a real-world application, your authentication backend would generate such a token at login / signup and hand it over to the mobile app. For more information, see the Tokens & Authentication page.
  3. We use the DemoChannelList component and initialize the channelListController controller with a ChannelListQuery. We’re using the default sort option which orders the channels by last_updated_at time, putting the most recently used channels on the top. For the filter, we’re specifying all channels of type messaging where the current user is a member. The documentation about Querying Channels covers this in more detail.
  4. We set the channelList as the root of a new UINavigationController and make it the root of our window

Project Permissions

Before we launch the app, let's add permissions to our app so that we can access the camera and gallery. Our message input UI component supports this by default.

  1. Select your project from the Project navigator on the left
  2. Open the Info tab for the ChatDemo target
  3. Add a new entry with "Privacy - Camera Usage Description" as key and "ChatDemo camera use" as value
  4. Add a new entry with "Privacy - Photo Library Usage Description" as key and "ChatDemo photo use" as value

Chat Features

Congrats on getting your chat experience up and running! Stream Chat provides you with all the features you need to build an engaging messaging experience:

  1. Offline support: send messages, edit messages and send reactions while offline
  2. Link previews: generated automatically when you send a link
  3. Commands: type / to use commands like /giphy
  4. Reactions: long-press on a message to add a reaction
  5. Attachments: use the paperclip button in MessageInputView to attach images and files
  6. Edit message: long-press on your message for message options, including editing
  7. Threads: start message threads to reply to any message

The Stream messaging API is powered by Go, RocksDB and Raft. The API tends to respond in less than 10ms and powers activity feeds and chat for over a billion end users.

Some features are hard to see in action with just one user online. You can open the same channel on the web and try user-to-user interactions like typing events, reactions, and threads.

Chat Message Customization

Next we will create a DemoChannelVC view controller so that we can show how you can customize the channel screen.

Under File > New > File > Cocoa Touch Class we create a new file called "DemoChannelVC", open it in our editor and make it inherit from ChatChannelVC.

swift
1
2
3
4
import StreamChatUI import UIKit class DemoChannelVC: ChatChannelVC {}

Open the SceneDelegate and make these two changes:

  1. Append this at the top after the other imports
swift
1
2
3
4
5
import StreamChatUI func applyChatCustomizations() { Components.default.channelVC = DemoChannelVC.self }
  1. Call applyChatCustomizations right before the initialization of ChatClient
swift
1
2
3
4
/// ... applyChatCustomizations() ChatClient.shared = ChatClient(config: config) /// ...

Let's now look at how we can customize the chat experience:

  1. Change message styles using attributes
  2. Create a custom attachment view
  3. Build your own views on top of the controllers provided by StreamChat

The first customization is fairly simple, components expose many attributes to handle the most common cases. Changing colors, font and images is very simple. The SDK allows you to make theming changes using the Appearance.default object.

To see this in practice, go back to SceneDelegate.swift and update the applyChatCustomizations function to looks like this:

swift
1
2
3
4
5
6
7
8
import StreamChatUI func applyChatCustomizations() { Appearance.default.colorPalette.background6 = .green Appearance.default.images.sendArrow = UIImage(systemName: "arrowshape.turn.up.right")! Components.default.channelVC = DemoChannelVC.self }

The Appearance object exposes all colors, fonts and images used by components. This makes it very easy to apply theme changes consistently across the entire application. Similar to Components you need to make changes to theming as early as possible in the application life cycle.

Before moving to the next section, let's look at how we added the channel screen to the application:

  1. First we added the channel list view controller with a navigation controller. This allows the SDK to navigate from the channel list to the channel screen.
  2. We added a new view controller and subclassed ChatChannelVC the VC that provides the channel functionality
  3. We configured StreamChat to use our DemoChannelVC class. The SDK exposes most of the UI configuration on the Components.default object. These kind of changes are best done when the application is starting.

Creating Custom Attachment Views

There may come a time when you have requirements to include things in your chat experience that we don't provide out-of-the-box. For times like this, we provide two main customization paths: you can either reimplement the entire message component and display a message how you like, or you can use custom attachment views. We'll look at this latter approach now.

You could use this to embed a shopping cart in your chat, share a location, or perhaps implement a poll. For this example, we'll keep it simple and customize the preview for images shared from Imgur. We're going to render the Imgur logo over images from the imgur.com domain.

Download the Imgur logo and drop the file inside Assets.xcassets using Xcode.

Also, for this example we will be using Nuke as our Image loading system dependency. You can add it to your project by going to File > Add Packages..., pasting the following URL in the search field at the top right: https://github.com/kean/Nuke and pressing "Add Package" button.

After that we are going to do the following:

  1. Create a custom view to display the image, and the Imgur logo overlay
  2. Create the AttachmentInjectorView class that adds our custom view to the message layout and initialize it with the content
  3. Create a custom AttachmentViewCatalog class, this is the class that is responsible for selecting the right view for a message attachment
  4. Register the custom catalog class to the Components object

The Custom Attachment View

You can place this code in a new file or simply add it to ViewController.swift

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import Nuke class ImgurImageAttachmentView: UIView { var content: ChatMessageLinkAttachment? { didSet { Task { try await updateContent() } } } lazy var imagePreview: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFill view.layer.cornerRadius = 8 view.clipsToBounds = true view.translatesAutoresizingMaskIntoConstraints = false view.isUserInteractionEnabled = false return view }() lazy var imgurLogoView: UIImageView = { let view = UIImageView() view.translatesAutoresizingMaskIntoConstraints = false return view }() override public init(frame: CGRect) { super.init(frame: frame) addSubview(imagePreview) NSLayoutConstraint.activate([ imagePreview.topAnchor.constraint(equalTo: topAnchor), imagePreview.bottomAnchor.constraint(equalTo: bottomAnchor), imagePreview.leadingAnchor.constraint(equalTo: leadingAnchor), imagePreview.trailingAnchor.constraint(equalTo: trailingAnchor) ]) let logo = UIImage(named: "imgur_logo")! addSubview(imgurLogoView) imgurLogoView.image = logo let logoWidth: CGFloat = 100 let logoRatio = logo.size.height / logo.size.width NSLayoutConstraint.activate([ imgurLogoView.bottomAnchor.constraint(equalTo: bottomAnchor), imgurLogoView.trailingAnchor.constraint(equalTo: trailingAnchor), imgurLogoView.widthAnchor.constraint(equalToConstant: logoWidth), imgurLogoView.heightAnchor.constraint(equalToConstant: logoWidth * logoRatio) ]) } required init?(coder: NSCoder) { super.init(coder: coder) } func updateContent() async throws { guard let url = content?.payload.assetURL else { return } let imageTask = ImagePipeline.shared.imageTask(with: url) imagePreview.image = try await imageTask.image } }

Here we added a UIView class called ImgurImageAttachmentView, this view class exposes a computed property content to hold and represent the attachment. Most of the code is just UIKit boilerplate code to render the labels and the Imgur logo on top of the image attachment.

The Custom AttachmentInjectorView

You can place this code right after the ImgurImageAttachmentView code.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ImgurImageAttachmentViewInjector: AttachmentViewInjector { let imgurImageAttachmentView: ImgurImageAttachmentView = { let view = ImgurImageAttachmentView() view.translatesAutoresizingMaskIntoConstraints = false return view }() override open func contentViewDidLayout(options: ChatMessageLayoutOptions) { contentView.bubbleContentContainer.insertArrangedSubview( imgurImageAttachmentView, at: 0, respectsLayoutMargins: true ) let constraint = imgurImageAttachmentView.widthAnchor.constraint(equalTo: contentView.widthAnchor) constraint.priority = .defaultLow constraint.isActive = true } override open func contentViewDidUpdateContent() { imgurImageAttachmentView.content = attachments(payloadType: LinkAttachmentPayload.self).first } }

The ImgurImageAttachmentViewInjector is a subclass of AttachmentViewInjector and wraps our custom view class. The two life-cycle methods contentViewDidLayout and contentViewDidUpdateContent are called by the SDK, this is where we set up our custom class with the right layout and with the content.

The Custom AttachmentViewCatalog

You can place this code right after the ImgurImageAttachmentViewInjector code.

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private func hasImgurLinkAttachment(message: ChatMessage) -> Bool { guard let imageAttachment = message.attachments(payloadType: LinkAttachmentPayload.self).first else { return false } guard let host = imageAttachment.assetURL?.host else { return false } return host.hasSuffix("imgur.com") } class MyAttachmentViewCatalog: AttachmentViewCatalog { override class func attachmentViewInjectorClassFor( message: ChatMessage, components: Components ) -> AttachmentViewInjector .Type? { if hasImgurLinkAttachment(message: message) { return ImgurImageAttachmentViewInjector.self } return super.attachmentViewInjectorClassFor(message: message, components: components) } }

The SDK uses the AttachmentViewCatalog to pick the appropriate AttachmentViewInjector for each message's attachments. Our subclass first checks if the message has any link attachment with images coming from the 'imgur.com' host and returns the custom injector for them.

Lastly, you need to configure the SDK to use the MyAttachmentViewCatalog. To do this you need to update the applyChatCustomizations function from SceneDelegate.swift.

swift
1
2
3
4
5
6
7
func applyChatCustomizations() { Appearance.default.colorPalette.background6 = .green Appearance.default.images.sendArrow = UIImage(systemName: "arrowshape.turn.up.right")! Components.default.channelVC = DemoChannelVC.self Components.default.attachmentViewCatalog = MyAttachmentViewCatalog.self }

When you run your app, you should now see the Imgur logo displayed over images from Imgur. You can test this by posting an Imgur link like this one: https://imgur.com/gallery/ro2nIC6.

This was, of course, a very simple change, but you could use the same approach to implement a product preview, shopping cart, location sharing, polls, and more. You can achieve lots of your message customization goals by implementing a custom attachment View.

If you need even more customization, you can also implement custom message views for the entire message object.

Creating a Typing Status Component

The channel list component shows typing indicators out of the box, in this example we are going to handle typing events ourselves and show our own.

The ChatChannelVC class is a delegate of the ChatChannelController controller and conforms to the ChatChannelControllerDelegate protocol.

This protocol allows delegates to receive channel updates including changes to the list of typing users, the latter via this method:

swift
1
2
3
4
func channelController( _ channelController: ChatChannelController, didChangeTypingUsers typingUsers: Set<ChatUser> )

Now that we know this, we can override the implementation from ChatChannelVC and write our own. This is how the DemoChannelVC.swift file should look like:

swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import StreamChat import StreamChatUI import UIKit class TypingIndicator: UILabel { func resetText() { text = "Nobody is typing" } } class DemoChannelVC: ChatChannelVC { var typingIndicator: TypingIndicator! override func setUpLayout() { super.setUpLayout() typingIndicator = TypingIndicator() view.addSubview(typingIndicator) typingIndicator.backgroundColor = .lightGray typingIndicator.translatesAutoresizingMaskIntoConstraints = false typingIndicator.resetText() NSLayoutConstraint.activate([ typingIndicator.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), typingIndicator.heightAnchor.constraint(equalToConstant: 30), typingIndicator.leadingAnchor.constraint(equalTo: view.leadingAnchor), typingIndicator.trailingAnchor.constraint(equalTo: view.trailingAnchor), messageListVC.view.topAnchor.constraint(equalTo: typingIndicator.bottomAnchor) ]) } override func channelController( _ channelController: ChatChannelController, didChangeTypingUsers typingUsers: Set<ChatUser> ) { let typingUsersWithoutCurrentUser = typingUsers .filter { $0.id != self.channelController.client.currentUserId } guard let typingUser = typingUsersWithoutCurrentUser.first else { typingIndicator.resetText() return } typingIndicator.text = "\(typingUser.name ?? typingUser.id) is typing..." } }

You can open the same channel on the web and try this using a different user.

Enabling Console Logs

By default console logs are disabled. But it's likely whilst you're building your app that you will need to enable logs at some point.

Let's go ahead and enable your console logs now.

swift
1
2
3
import StreamChat LogConfig.level = .info

By setting your log level to .info means you will get access to all console logs provided by the SDK.

The SDK supports many more logs, and you can read more about them here.

Congratulations!

In this iOS in-app messaging tutorial, you learned how to build a fully functional chat app with Swift and UIKit. You also learned how easy it is to customize the behavior and build any type of chat or messaging experience.

Remember, you can also check out the completed app for the tutorial on GitHub.

If you want to get started on integrating chat into your own messaging app, sign up for a free Chat trial, and get your own API key to build with!

To recap, our iOS Chat SDK consists of two frameworks which give you an opportunity to interact with Stream Chat APIs on a different level:

  • StreamChat - The official low-level Swift SDK for Stream Chat. It allows you to make API calls and receive events whenever something changes on a user or channel that you’re watching.
  • StreamChatUI - Builds on top of the low level client and provides fully custom UI, this library is your best starting point.

The underlying chat API is based on Go, RocksDB, and Raft. This makes the in-app chat experience extremely fast with response times that are often below 10ms.

Final Thoughts

In this chat app tutorial we built a fully functioning iOS messaging app with our SDK component library. We also showed how easy it is to customize the behavior and the style of the iOS chat app components with minimal code changes.

Both the chat SDK for iOS and the API have plenty more features available to support more advanced use-cases such as push notifications, content moderation, rich messages and more. You may also want to read our SwiftUI tutorial too.

Give us feedback!

Did you find this tutorial helpful in getting you up and running with your project? Either good or bad, we're looking for your honest feedback so we can improve.

Start coding for free

No credit card required.
If you're interested in a custom plan or have any questions, please contact us.