Building a Social Network with SwiftUI – Part 2 (Direct Messaging)

In the second part of our series, we'll implement direct messaging between users by integrating Stream Chat. This post assumes you've followed along with part 1.

Leveraging our code from part 1, we'll modify the backend to generate a Stream Chat frontend token so our mobile application can communicate directly with Stream's Chat API. The Stream Chat frontend token securely authenticates a client application with the Stream Chat so it can directly talk with the API without going through our backend. This token is created and stored as a part of the login process which is defined in part 1.

For this part, we'll use Stream's UI components instead of building the views ourselves to see the great UI experience we get out of the box.

For the backend, we'll add in the Stream Chat JavaScript library.

For the mobile app, we'll build it with Swift wrapping the Stream Chat Swift libraries.

The app goes through these steps to allow a user to chat with another:

  • User navigates to the user list and clicks on their name or chat icon. The mobile application joins a 1-on-1 chat channel between the two users.
  • The app queries the channel for previous messages and indicates to Stream that we'd like to watch this channel for new messages. The mobile app listens for new messages.
  • The user creates a new message and sends it to the Stream API.
  • When the message is created, or a message from the other user is received, the mobile application consumes the event and displays the message.

Since we're relying on the Stream mobile libraries to do the heavy lifting, most of this work happens in the Stream Chat UI Components. If you'd like to follow along, make sure you get both the backend and mobile app running part 1 before continuing. Please refer to the respective package management files (Podfile, package.json) to see what versions of the libraries we're using.

Configuring Stream Chat

To start, we need to configure Stream chat. We'll add on to the login flow that we created in part 1. Recall when we login we set up Stream. This instance of Stream is for the Stream Feed API. For chat, we need to set up a different client to interact with the Stream Chat API. Like before, we need an endpoint that creates our Stream Chat frontend token, so our mobile application can communicate directly with the Stream API. Let's check out our new endpoint:

https://gist.github.com/nparsons08/e34ef2eaa56e7299ff71b1a630cc1080

This endpoint generates the Stream Chat frontend token. It also creates or updates the user to associate the token with the user. This is so Stream knows who's talking to it when actions are taken in the mobile application. Using this endpoint, we can modify our login flow to request these credentials as well. We modify .setupFeed from part one to call to .setupChat after we're done with the feed setup. Here's the new version of setupFeed:

https://gist.github.com/nparsons08/cbda19fd7a4c5d964e7189c045d778c8

This code is largely the same except for the last line where we call .setupChat instead of setting the isAuthed flag. Here's .setupChat:

https://gist.github.com/nparsons08/7ef590343d2eb8bd0e392acd4e38c976

Here we call the endpoint we set up previously and get our frontend token. We use this to initialize the Chat Client singleton for use later. We then indicate we're authenticated via the isAuthed flag so the application knows we're all set up and ready to go.

Selecting a User to Chat

Step 1: Adding Navigation

For this app, the chat view is it's own screen, meaning there is no tab bar when chatting. To support this we first wrap our TabView inside of ContentView with a NavigationView:

https://gist.github.com/nparsons08/7330945173b847083925539f3448e0d2

This NavigationView wrapper allows us to push new views on the stack and provides transitions and a back button. Since we have a nice navigation bar, we add a small title to the application. The list of people now looks like this (ignore the chat bubble for now as it's explained next):

User List

Notice the default navigation bar at the top now. This is present on all screens now and automatically provides a back button if we push a new view on the navigation stack, such as our 1-on-1 chat. This allows the user to get back to the main view when they're done chatting. Let's see push a chat view on the stack.

Step 2: Starting a Chat

Now that we have a NavigationView at the top of our hierarchy, we can build a NavigationLink to show the chat. Since we'd like to preserve the follow action, we'll place a start chat icon next to the follow icon. We'll also make the whole row clickable. This means that unless the user explicitly clicks the follow icon (from part 1), we'll start a chat. Here's PeopleView with these udpates:

https://gist.github.com/nparsons08/e6845447a8e2b59569ea54fa2333f03e

The navigation is done by the NavigationLink embedded in the row. Since we give the content of the NavigationLink a Spacer this forces the icons to align right. Each link is tagged with its index allowing us to "select" it when a user clicks the name or the message icon. This allows each element to be clickable by assigning the tag, which triggers the NavigationLink to be selected.

When the NavigationLink is selected, by setting the tag or clicking on the Spacer, it pushes a PrivateChatView (defined next) onto the navigation stack. By using these built-in navigation views, we get a nice default transition (swipe animation) to the next view with a back button built for us. Now we simply add our chat view in and we're done!

Viewing a Chat

Step 1: Creating a Channel

First, we create a channel via Streams' library. The job of PrivateChatChannel is to create the channel and feed it to our StreamChatView (defined below). Let's look at the code for PrivateChatView:

https://gist.github.com/nparsons08/e5e98b22477a7c4bfe33713e413657ff

We check if the channel has been initialized. If it's not, we show a loading screen and perform loadChannel. This calls to account.createPrivateChannel. Here's the implementation:

https://gist.github.com/nparsons08/ec7b337fe1912e1173917fdffd4fdb9d

This function generates a unique channel id and sets up a private channel that contains only the two members that are chatting. It returns the channel via the completion.

Now that the view has a channel, it can boot a StreamChatView. This is a view that wraps Steam's ChatViewController.

Step 2: Wrapping Stream's UI Components

In the first part of this series, we built our timeline view from scratch. In this part, we'll wrap Stream's UI components to make our jobs easier. Stream provides convenient components to render a great chat experience with little work. There are no SwiftUI components yet, however, using Swift's built-in support for UIControllers we can easily wrap Stream's library. Let's see the code for StreamChatView:

https://gist.github.com/nparsons08/5c2dc9d2bfb72eef8e35998b0e429d1b

We use SwiftUI's UIViewControllerRepresentable which allows us to create and manage a UIViewController. Since Stream's high-level components are UIViewControllers this is exactly what we need to bring them into SwiftUI.

All Stream's component needs to render is a ChannelPresenter. There's a lot of oppurtunities to customize this view, but we'll keep it simple for now. Since we instantiated this object when creating the channel, we simply use the one passed along to us by PrivateChatChannel. With that object, we create a ChatViewController instance, which comes from Stream's library, and give it the ChannelPresenter. Now we get a full chat experience with only a few lines of work:

Chat Populated

And we're done. Stream's built-in views makes it straightforward to build a great chat experience!

TutorialsChatFeeds