Creating a chat screen in SwiftUI (5/7)

A chat app wouldn't be a chat app without a chat screen. In this part of the SwiftUI course, you'll put everything you've learned so far together, and build a cool looking chat screen.

Marin Benčević • Apr 21, 2020

A chat app wouldn't be a chat app without a chat screen. In this part of the SwiftUI course, you'll put everything you've learned so far together, and build a cool looking chat screen.

Here's what you'll build:

The chat screen consists of two main views: a list of messages and a text field. Each message contains the user's avatar and a text bubble displaying the message. The text area holds a text field and a send button side-by-side.

Looking at this screen, it might seem like a daunting task to build it. Don't worry! We'll tackle it piece-by-piece. Once you put those pieces together, you'll be surprised at how great everything turned out! Let's get going.

As always, you can find a link to the finished project code on GitHub.

Modifying the avatar

Earlier in the course, you created an avatar view that you can reuse across your app. Now, it's time to reap the benefits of that investment. Instead of recreating a whole new view, you'll slightly tweak the avatar view to fit the needs of the chat message rows.

You'll change the avatar view so that the online indicator can be hidden. Open AvatarView.swift and add a new property to the class:

private let showsOnlineStatus: Bool

You'll use this variable to determine whether or not the online badge will be shown. Set this property to true at the bottom of a new initializer that you'll add to the class:

Next, add another initializer to the class, this time without isOnline:

You will call this initializer when you don't need to know if the user is online or not.

Finally, make a few changes to body to only show the online badge if showsOnlineStatus is set to true:

With that in place, you can get started on creating the message item.

Creating the view for chat messages

By the end of this section, you'll create a view that shows each message the user sends or receives.

When a user sends a message, it will have a blue background and be aligned to the right. If the user receives a message, it will have a white background and come from the left. When you receive or send a couple of messages, only the last one of those will show the avatar. Let's get started!

First, you'll create a new model struct for you messages. Crate a new plain Swift file and name it Message.swift. Add the following to the file:

Just like Contact, you'll make Message identifiable, so that you can later display it in a list view. Now you can start creating the view that shows this message.

Create a new SwiftUI view file and name it ChatMessageRow.swift. Change the struct to the following:

The struct has three properties. One is the message you'd like to show. The second property determines if the message is incoming or if it was sent out from the currently logged in user. The final property determines if this message was last in a list of multiple messages from a single contact.

The latter two properties will determine the look of this view.

Displaying the content of the message

Instead of having a 300-lines-long body behemoth, you'll make this view piece by piece. You'll create a few computed variables where you'll return parts of the view and then combine those parts in body.

The first part will be the most important part of a message: the content. Add the following computed property to the bottom of the class:

Just like body, you can create computed properties that return some View, and use this pattern to clean up your code so that it's more readable. For now, the text is a simple Text with some padding and styling applied.

Next, fill up body with a horizontal stack containing the avatar view and the text:

Alongside the two views in the stack, you add a spacer so that everything is aligned to the left. By setting the stack's alignment to .bottom, you make sure the items start at the bottom of the stack.

Before I show you what this looks like, let's modify the preview a bit. Scroll down to the PreviewProvider and add the following property to the struct:

This is a dummy message that you'll use to preview the view. Next, modify previews to show three different permutations of what your view can look like:

You create one outgoing, one incoming, and one message row where the message is not the last message from that contact. Take a look at the preview to see your work so far:

That's a good start! You're showing the avatar and the message, but you're missing the bubble around the message.

Showing a chat bubble

To show the chat bubble, you'll use a rounded rectangle view that you'll set as the background of the text view.

To start, add another computed property to ChatMessageRow, right above text:

Remember the shape views I mentioned earlier in the course? Another one of those is RoundedRectangle, perfect for this use case: A chat bubble.

You'll set this chat bubble as the background of the text. Inside text, call the background method with the chat bubble:

In SwiftUI, backgrounds can be infinitely complex. In this case, you're setting it to a rounded rect. You can also set it to colors, image views, other shape views or any other SwiftUI view!

The text now has a background, but to make it a true speech bubble it needs a tail from the user's avatar to the message, like in comic books.

To add this, you'll create a triangular view that you'll place between the avatar and the message in the stack. Add a new function to the struct:

This function receives a width and a height for the chat bubble tail, as well as whether the message is incoming or not. You'll use this information to construct the bubble and return it from the function.

Next, add the following code to the function:

You use a Path view to construct the triangle. Path is a special type of view that lets you create any arbitrary shape that you can think of.

If this sounds complex, don't worry. Path has a bunch of helper methods to construct shapes like straight lines, arcs, curves, rectangles, ellipses and more. By combining those simpler shapes, you can end up with shapes that would make Kandinsky jealous.

When constructing a path, think of it like drawing with an imagined pen. You move the pen to its start position by calling move(to:). Once it's in position, you can press it down and draw by calling one of the addX methods, where X can be anything from line, arc, curve and other shapes.

In this case, you first move the path to the center-left position of the view. You then draw a line to the top-right corner and then draw a line to the bottom-right corner. Finally, you call closeSubpath which closes the path by drawing a line back to the start position.

Now that you have the tail, add it to body:

The shape is good, but the view needs some tweaks. We need to trick the user into thinking the tail is a part of the message bubble. Add the following method calls to the bottom of chatBubbleTriangle:

{% c-block language=“swift” %}
.zIndex(10)
.clipped()
.padding(.trailing, -1)
.padding(.leading, 10)
.padding(.bottom, 12)
{% c-block-end %}

You modify the tail's z-index so that it's above the chat message. You then clip the view so that there's no shadow at the right edge. Finally, you adjust the padding so that it's overlapping with the chat message by one point.

Now it looks like a real speech bubble! This makes you one step closer to a real app since no chat app would be complete without speech bubbles. Let's tweak them a little bit more.

Adding color to the speech bubbles

Next, you'll color outgoing messages blue, and incoming messages white. Start by changing the text color in the text computed variable, so that it's visible on both backgrounds:

Make a similar modification to the chat bubble, except you can color it blue if the message is outgoing:

Finally, you need to match the tail's color to the bubble. In chatBubbleTriangle, replace the first method call to fill with the following line:

The preview now shows a blue message when it's not incoming. Nice!

Chaining messages

As we discussed earlier, if the same user sends a couple of messages, you'll only show the avatar at the last message. For other messages, you'll show a spacer of the same width as the avatar in its place.

Modify body to add this change:

If the message is last from this contact, you show the avatar and the tail. Otherwise, you show a blank space. Earlier you learned that spacers take up as much space as they can. By modifying the spacer's frame, you make sure it has a fixed width.

Now, only the last message in a chain shows the avatar. We're almost there!

Reversing a horizontal stack in SwiftUI

One final thing you need to tweak is to align outgoing messages to the right. To do this, you'll need to reverse the order of items in the HStack, as well as flip the chat bubble's tail.

You'll start by flipping the tail. Since you built it as a path, you can reverse the x coordinates of the path to mirror the triangle.

In chatBubbleTriangle, reverse the x-axis of the path if the message is outgoing by replacing the Path initializer with the following code:

You'll also need to reverse the padding by replacing the last three lines of the function with the following:

Now your tail is sufficiently flipped.

Next, you'll move on to the stack view by reversing the order of the items in the stack.

Right now, there's no easy way to do this in SwiftUI. The way to do this is to add an if check inside the stack, and repeat the views in the reverse order.

Not the cleanest solution, but it's the best we can do right now.

Your messages are now aligned to the right if they're coming from the current user. This concludes creating the message row. You can now take a small break by creating the text field. Or, take a small break, and then create the text field. :)

Creating the chat text field

A chat screen is two things: The messages and a text field. In this section, you'll make a nice looking text field to let the user type in their messages.

Before we get started, you'll need to download another image. Download the send icon from here. Once downloaded, drag and drop the image inside you Assets.xcassets file and name it send_message.

Start creating the text field by creating a new SwiftUI View file called ChatTextField.swift. Add two properties and a method to the struct:

The text field will receive a callback to call when the send button gets tapped. It will track the entered text inside a state variable, and pass the text via the callback back to the main chat screen.

Next, modify the preview so that it shows the text field with a bit of spacing on the top:

Now you can start working on the view. Inside body, add a horizontal stack that contains a text field and a button to send the message.

You fix the text field's size to 60 points. In the button, you show the send icon and make sure its size is 25 by 25 points. Finally, you add a bit of padding to the whole stack and give it a white background.

You should do one final modification by adding a shadow to the top of the view. Because you want the shadow only on top, you'll add a new thin rectangular view to the top, and apply a shadow to that rectangle.

To add the rectangle, you first wrap the whole view inside of a vertical stack. You add the rectangle to the top of the stack and apply a shadow to the rectangle. You'll also set the VStack's frame to be 60 points high.

Looking good! You now have a text field and a view for each message. It's time to assemble them!

Putting it all together in a chat view

Now, it's time to cash in all of your hard work so far, and put together the pieces to form a great looking chat view.

Create a new SwiftUI view file named ChatView.swift. Add the following properties to the struct:

The chat view will receive two users: One that's currently logged in, and one that the user will chat with.

You'll need to modify the previews to pass values for these two properties:

Next, scroll back to the ChatView struct and add the following state property:

These are the messages you'll show on the screen. Right now, you're hard-coding them in the struct. Later, you'll be listening to a WebSocket channel and adding messages to this array as they come in — but that's in a later part of this course.

Now you can add a list of messages to the view. Add a body to the struct:

As you learned earlier in the course, you combine a ForEach with a List to create a list of ChatMessageRows. You know the message is incoming if the message's sender is different from the current user.

Looks like you're having issues with the table view's separators. You've already learned a trick to deal with these: Using UIAppearance. Add an initializer to the top of the struct:

In the initializer, you use the global appearance proxies for the table view to remove the separators. You also remove all background colors from both the table view and the cells. This lets you change the background color in SwiftUI.

Remember that the chat message looks different for chained messages? You'll have to add a way to determine if each message was the lest message sent by a user or just another message in the chain. Add the following method to the struct:

‍You fetch the message at that index, as well as the next message. If the next message has a different sender, the current message is the last in the chain.

Next, pass the result of this function to each chat message row. You'll also use this function to modify the spacing between the rows.

If the message is the last in a chain, you'll add a larger spacing to the bottom. You'll also add a bigger top spacing to the first row in the list so that it doesn't bump into the navigation bar.

The list is looking great now! Let's move on to adding the text field you created earlier to the view.

Start by creating a new function that will get called when the text field's send button is tapped:

Next, wrap body in a VStack and add the text field to the bottom of the stack:

When wrapping views inside a stack, Xcode gives you several tricks you can use. If you Command-Click the view, you can select Embed in VStack to wrap a VStack around that view. If you decide to do it manually, by selecting a piece of text and hitting Control-i Xcode will reindent the text for you. Pretty nifty!

Since you made a reusable text field, this one line of code is all you need to add the text field to the view. Good job, you!

Changing the background of a SwiftUI List

Finally, you'll add a background to the screen. Previously, you used the background method to color a view. Another way to do this is to use a ZStack. After all, a ZStack stacks views one on top of the other. If the top view has a clear background, the bottom view will show through and act as a background.

Add the whole body to a ZStack, where you show the background color at the bottom of the stack:

In SwiftUI, Color is not only a struct that represents a color. It can also be used as a standalone view by itself! You use this fact to add a background color to the ZStack. Because you set the table view's and the cells' background color to .clear, the background shows through them.

Conclusion

Our chat app is starting to take shape! We now have a way to log in, pick a person to chat with and, finally, a way to chat with them. At least, in theory.

The remainder of this course will deal with less visual aspects of your app. The next section will deal with propagating state and data through your app and sharing data between SwiftUI views. Once you master that, you'll move on to networking and hooking your app up to the Internet.

So, unless you want to keep chatting with dummy users forever, keep reading! :)

Marin Benčević

CometChat

Marin, iOS developer from Croatia 🇭🇷. Follow me on Twitter @marinbenc

Try out CometChat in action

Experience CometChat's messaging with this interactive demo built with CometChat's UI kits and SDKs.