Creating a chat screen in SwiftUI
Tags: ios, swift, swiftui
Published: 2020-02-11 07:52:42

---
title: Creating a chat screen in SwiftUI
published: true
description: 
tags: ios, swift, swiftui
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/wbfvrzv2yzmyh1zejse3.png
series: CometChat SwiftUI course
canonical_url: https://www.cometchat.com/tutorials/creating-a-chat-screen-in-swiftui-4-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.

Here's what you'll build:

![Making a SwiftUI chat screen](https://dev-to-uploads.s3.amazonaws.com/i/f0v3rzatohdzv9hhikzb.png)

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](https://github.com/cometchat-pro-tutorials/swift-ui-chat).

## 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:

```swift
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:

```swift
init(url: URL?, isOnline: Bool) {
  self.url = url
  self.isOnline = isOnline
  showsOnlineStatus = true
}
```

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

```swift
init(url: URL?) {
  self.url = url
  self.isOnline = false
  showsOnlineStatus = false
}
```

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`:

```swift
var body: some View {
  ZStack {
    Image("avatar_placeholder0")
      .resizable()
      .frame(width: 37, height: 37)
    
    // Wrap the Circle in an if block:
    if showsOnlineStatus {
      Circle()
        .frame(width: 10, height: 10)
        .foregroundColor(isOnline ? .green : .gray)
        .padding([.leading, .top], 25)
    }
  }
}
```

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

> I am creating this course in collaboration with CometChat, a modern chat platform to help you add chat to you Swift app. During the next few weeks, we’ll be releasing installments of our free SwiftUI course here on dev.to! In the course, you’ll dive deep into SwiftUI by building a real-world production-scale chat app, learning SwiftUI in a practical way, on a scale larger than a simple example app. Follow me to get notified of future parts of this course! You can also follow [@CometChat](https://twitter.com/cometchat) on Twitter see the course of [CometChat’s blog](https://www.cometchat.com/blog/).

[![](https://thepracticaldev.s3.amazonaws.com/i/avpunu4hqp1ut7enf5tq.png)](https://www.cometchat.com)

## 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.

![Making a SwiftUI chat screen](https://dev-to-uploads.s3.amazonaws.com/i/xa969xig0h45fyfj1sw9.png)

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:

```swift
import Foundation

struct Message: Identifiable {
  let id: Int
  let text: String
  let contact: Contact
}
```

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:

```swift
struct ChatMessageRow: View {
  
  let message: Message
  let isIncoming: Bool
  let isLastFromContact: Bool

}
```

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.

[TODO: Screenshot]

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:

```swift
private var text: some View {
  Text(message.text)
    .padding(10)
    .modifier(BodyText())
}
```

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:

```swift
var body: some View {
  HStack(alignment: .bottom, spacing: 0) {
    AvatarView(url: message.contact.avatar)
    text
    Spacer()
  }
}
```

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:

```swift
private static let chatMessage = Message(
  id: UUID().hashValue,
  text: "Pellentesque ipsum. Mauris elem enes tumen mauris vitae tortor. Pellentesque ipsum.",
  contact: Contact(name: "Name", avatar: nil, id: "id", isOnline: true))
```

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:

```swift
static var previews: some View {
  Group {
    ChatMessageRow(
      message: chatMessage,
      isIncoming: true,
      isLastFromContact: true)
      .previewLayout(.fixed(width: 300, height: 200))
    
    ChatMessageRow(
      message: chatMessage,
      isIncoming: true,
      isLastFromContact: false)
      .previewLayout(.fixed(width: 300, height: 200))
    
    ChatMessageRow(
      message: chatMessage,
      isIncoming: false,
      isLastFromContact: true)
      .previewLayout(.fixed(width: 300, height: 200))
  }
}
```

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:

![Creating the message item of a SwiftUI chat UI](https://dev-to-uploads.s3.amazonaws.com/i/45i78xpyc2oecichave2.png)

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`:

```swift
private var chatBubble: some View {
  RoundedRectangle(cornerRadius: 6)
    .foregroundColor(.white)
    .shadow(color: .shadow, radius: 2, x: 0, y: 1)
}
```

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:

```swift
Text(message.text)
  .padding(10)
  .modifier(BodyText())
  .background(chatBubble)
```

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_!

![Making a chat bubble in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/jchjcyh1xjy05s8n17ic.png)

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:

```swift
private func chatBubbleTriange(
  width: CGFloat, 
  height: CGFloat, 
  isIncoming: Bool) -> some View {

}
```

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:

```swift
Path { path in
  path.move(to: CGPoint(x: 0, y: height * 0.5))
  path.addLine(to: CGPoint(x: width, y: height))
  path.addLine(to: CGPoint(x: width, y: 0))
  path.closeSubpath()
}
.fill(Color.white)
.frame(width: width, height: height)
.shadow(color: .shadow, radius: 2, x: 0, y: 1)
```

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`:

```swift
var body: some View {
  HStack(alignment: .bottom, spacing: 0) {
    AvatarView(url: message.contact.avatar)
    
    chatBubbleTriange(width: 15, height: 14, isIncoming: isIncoming)
    
    text
    
    Spacer()
  }
}
```

![Making a chat bubble in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/gydbj0vgsjbf2p9l5a18.png)

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`:

```swift
.zIndex(10)
.clipped()
.padding(.trailing, -1)
.padding(.leading, 10)
.padding(.bottom, 12)
```

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.

![Making a chat bubble in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/wnei63git5s7rvrojyf8.png)

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:

```swift
Text(message.text)
  .padding(10)
  .foregroundColor(isIncoming ? .body : .white)
  .modifier(BodyText())
  .background(chatBubble)
```

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

```swift
private var chatBubble: some View {
  RoundedRectangle(cornerRadius: 6)
    .foregroundColor(isIncoming ? .white : .cometChatBlue)
    .shadow(color: .shadow, radius: 2, x: 0, y: 1)
}
```

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:

```swift
.fill(isIncoming ? Color.white : Color.cometChatBlue)
```

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

![Making a chat bubble in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/ykrbft0ev99zburil4py.png)

### 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:

```swift
var body: some View {
  HStack(alignment: .bottom, spacing: 0) {
    if isLastFromContact {
      AvatarView(url: message.contact.avatar)
      chatBubbleTriange(width: 15, height: 14, isIncoming: isIncoming)
    } else {
      Spacer().frame(width: 61)
    }
    text
    Spacer()
  }
}
```

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.

![Making a chat bubble in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/ag56j2j4tpbmfg2d8cp0.png)

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:

```swift
Path { path in
  path.move(to: CGPoint(x: isIncoming ? 0 : width, y: height * 0.5))
  path.addLine(to: CGPoint(x: isIncoming ? width : 0, y: height))
  path.addLine(to: CGPoint(x: isIncoming ? width : 0, y: 0))
  path.closeSubpath()
}
```

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

```swift
.padding(.trailing, isIncoming ? -1 : 10)
.padding(.leading, isIncoming ? 10 : -1)
.padding(.bottom, 12)
```

Now your tail is sufficiently flipped.

![Making a chat bubble in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/d4vg3vdv5ejbac7tewui.png)

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.

```swift
HStack(alignment: .bottom, spacing: 0) {
  if isIncoming {
  
    if isLastFromContact {
      AvatarView(url: message.contact.avatar)
      chatBubbleTriange(width: 15, height: 14, isIncoming: true)
    } else {
      Spacer().frame(width: 61)
    }
    text
    Spacer()
    
  } else {
  
    Spacer()
    text
    if isLastFromContact {
      chatBubbleTriange(width: 15, height: 14, isIncoming: false)
      AvatarView(url: message.contact.avatar)
    } else {
      Spacer().frame(width: 61)
    }
  }
}
```

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

![Making a chat bubble in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/nozdrjz3jbbey4y25q6l.png)

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](https://github.com/cometchat-pro-tutorials/swift-ui-chat/raw/master/CometChat/CometChat/Assets.xcassets/send_message.imageset/send_message.png). 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:

```swift
struct ChatTextField: View {
  
  let sendAction: (String) -> Void
  
  @State private var messageText = ""

  private func sendButtonTapped() {
    sendAction(messageText)
    messageText = ""
  }

}
```

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:

```swift
static var previews: some View {
  VStack {
    Spacer()
    ChatTextField(sendAction: { _ in })
  }.previewLayout(.fixed(width: 300, height: 80))
}
```

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.

```swift
var body: some View {
  HStack {
    TextField("Type something", text: $messageText)
      .frame(height: 60)
    
    Button(action: sendButtonTapped) {
      Image("send_message")
        .resizable()
        .frame(width: 25, height: 25)
        .padding(.leading, 16)
    }
  }
  .padding([.leading, .trailing])
  .background(Color.white)
}
```

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.

![Creating a chat text field in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/92m7a1qwcvdsh9txivl2.png)

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.

```swift
var body: some View {
  VStack(spacing: 0) {
    Rectangle()
      .frame(height: 1)
      .foregroundColor(.white)
      .shadow(color: .shadow, radius: 3, x: 0, y: -2)

    HStack {
      ...
    }
    .padding([.leading, .trailing])
    .background(Color.white)
  }
  .frame(height: 60)
}
```

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.

![Creating a chat text field in SwiftUI](https://dev-to-uploads.s3.amazonaws.com/i/uarloz89t9mlw8ew30eu.png)

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:

```swift
struct ChatView: View {
  
  let currentUser: Contact
  let receiver: Contact

}
```

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:

```swift
static var previews: some View {
  ChatView(
    currentUser: Contact(name: "Me", avatar: nil, id: "me", isOnline: true),
    receiver: Contact(name: "Other", avatar: nil, id: "other", isOnline: true))
}
```

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

```swift
@State private var messages: [Message] = [
  Message(id: 0, text: "Morbi scelerisque luctus velit", contact: Contact(name: "Name", avatar: nil, id: "id", isOnline: true)),
  Message(id: 1, text: "Pellentesque ipsum. Mauris elem enes tumen mauris vitae tortor. Pellentesque ipsum. ", contact: Contact(name: "Name", avatar: nil, id: "id", isOnline: true)),
  Message(id: 2, text: "Phasellus enim erat esi, vestibulum veles?", contact: Contact(name: "Name", avatar: nil, id: "me", isOnline: true)),
  Message(id: 3, text: "Lorem ipsum dolor sit amet, consectetuer adipiscing elit.", contact: Contact(name: "Name", avatar: nil, id: "id", isOnline: true)),
  Message(id: 4, text: "Mauris tincidunt sem", contact: Contact(name: "Name", avatar: nil, id: "me", isOnline: true)),
]
```

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:

```swift
var body: some View {
  List {
    ForEach(0..<messages.count, id: \.self) { i in
      ChatMessageRow(
        message: self.messages[i],
        isIncoming: self.messages[i].contact.id != self.currentUser.id,
        isLastFromContact: true)
    }
  }
}
```

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

![Removing separators from a SwiftUI List](https://dev-to-uploads.s3.amazonaws.com/i/k7tkvxcrsanzqh0s0maq.png)

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:

```swift
init(currentUser: Contact, receiver: Contact) {
  self.currentUser = currentUser
  self.receiver = receiver
  
  UITableView.appearance().tableFooterView = UIView()
  UITableView.appearance().separatorStyle = .none
  UITableView.appearance().backgroundColor = .clear
  UITableViewCell.appearance().backgroundColor = .clear
}
```

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.

![Removing separators from a SwiftUI List](https://dev-to-uploads.s3.amazonaws.com/i/2bt63jh89bwvvp3ld2jj.png)

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:

```swift
private func isMessageLastFromContact(at index: Int) -> Bool {
  let message = messages[index]
  let next = index < messages.count - 1 ? messages[index + 1] : nil
  return message.contact != next?.contact
}
```

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.

```swift
List {
  ForEach(0..<messages.count, id: \.self) { i in
    ChatMessageRow(
      message: self.messages[i],
      isIncoming: self.messages[i].contact.id != self.currentUser.id,
      isLastFromContact: self.isMessageLastFromContact(at: i))
      .listRowInsets(EdgeInsets(
        top: i == 0 ? 16 : 0,
        leading: 12,
        bottom: self.isMessageLastFromContact(at: i) ? 20 : 6,
        trailing: 12))
  }
}
```

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.

![Making a SwiftUI chat screen](https://dev-to-uploads.s3.amazonaws.com/i/xa969xig0h45fyfj1sw9.png)

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:

```swift
private func onSendTapped(message: String) {
  // TODO: Send message
}
```

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

```swift
VStack {
  List {
    ...
  }
  
  ChatTextField(sendAction: onSendTapped)
}
```

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!

![Making a SwiftUI chat screen](https://dev-to-uploads.s3.amazonaws.com/i/gb3uzvc2u7n63ldci1l1.png)

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:

```swift
ZStack {
  Color.background.edgesIgnoringSafeArea(.top)
  
  VStack {
    List {
      ...
    }
    
    ChatTextField(sendAction: onSendTapped)
  }
}
```

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.

![Making a SwiftUI chat screen](https://dev-to-uploads.s3.amazonaws.com/i/f0v3rzatohdzv9hhikzb.png)

## 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! :)