Chat App development tutorial for iOS using SwiftUI

Jakir Hossain
5 min readOct 9, 2023

We are going to develop a chat app starter template using SwiftUI. Here is the final output of our project:

Chat app starter template

We will create our message model first. Create a file called Message.swift. For simplicity, our message model will contain these fields:

import Foundation

struct Message: Hashable {
var id = UUID()
var content: String
var isCurrentUser: Bool
}

Then, we are going to create the UI files. Let’s create a file called MessageCell.swift. Here, we will create a UI for a single message. This is the baseline of our project. It will contain a single text view.

        Text("This is a single message cell.")
.padding(10)
.foregroundColor(Color.white)
.background(Color.blue)
.cornerRadius(10)

Output will be like this:

Now this is for current users. If we receive a message from another user, we have to differentiate the view. We can use different colors for that, like this:

        Text("This is a single message cell.")
.padding(10)
.foregroundColor(Color.black)
.background(Color(UIColor.systemGray6))
.cornerRadius(10)

The output will be:

Now we can combine both in a single Text view using conditional. If the user is a current user, we will use a blue background and white text color. Otherwise, we will use a light gray background and black text color. Here is the full code of the MessageCell.swift file:

import SwiftUI

struct MessageCell: View {
var contentMessage: String
var isCurrentUser: Bool

var body: some View {
Text(contentMessage)
.padding(10)
.foregroundColor(isCurrentUser ? Color.white : Color.black)
.background(isCurrentUser ? Color.blue : Color(UIColor.systemGray6 ))
.cornerRadius(10)
}
}

#Preview {
MessageCell(contentMessage: "This is a single message cell.", isCurrentUser: false)
}

If you use Facebook Messenger, you’ll notice that senders have avatars along with messages. Therefore, we can include the avatar in our message cell. For that, let’s create a new file called MessageView.swift:

import SwiftUI

struct MessageView : View {
var currentMessage: Message

var body: some View {
HStack(alignment: .bottom, spacing: 10) {
if !currentMessage.isCurrentUser {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 40, height: 40, alignment: .center)
.cornerRadius(20)
} else {
Spacer()
}
MessageCell(contentMessage: currentMessage.content,
isCurrentUser: currentMessage.isCurrentUser)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
}
}


#Preview {
MessageView(currentMessage: Message(content: "This is a single message cell with avatar. If user is current user, we won't display the avatar.", isCurrentUser: false))
}

Here, we excluded the avatar from the current user. Output:

Now we need some dummy data. Let’s hard code some data in Message.swift file:

import Foundation

struct Message: Hashable {
var id = UUID()
var content: String
var isCurrentUser: Bool
}

struct DataSource {

static let messages = [

Message(content: "Hi there!", isCurrentUser: true),

Message(content: "Hello! How can I assist you today?", isCurrentUser: false),
Message(content: "How are you doing?", isCurrentUser: true),
Message(content: "I'm just a computer program, so I don't have feelings, but I'm here and ready to help you with any questions or tasks you have. How can I assist you today?", isCurrentUser: false),
Message(content: "Tell me a joke.", isCurrentUser: true),
Message(content: "Certainly! Here's one for you: Why don't scientists trust atoms? Because they make up everything!", isCurrentUser: false),
Message(content: "How far away is the Moon from the Earth?", isCurrentUser: true),
Message(content: "The average distance from the Moon to the Earth is about 238,855 miles (384,400 kilometers). This distance can vary slightly because the Moon follows an elliptical orbit around the Earth, but the figure I mentioned is the average distance.", isCurrentUser: false)

]
}

In a real app, maybe you will pull this data from the server. Now we can loop through these data and populate the message view. Open ContentView.swift and replace pre generate Text and Image view with this:

   List(messages, id: \.self) { message in
MessageView(currentMessage: message)
}

In preview or if we run the app we will see:

Pretty easy right?

You can hide list row separators like this:

 List(messages, id: \.self) { message in
MessageView(currentMessage: message)
}

We need to include a text field for typing the new message and a button for sending the message.

          
HStack {
TextField("Send a message", text: $newMessage)
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "paperplane")
}
}

From the button, we will call a function named sendMessage.

    func sendMessage() {

if !newMessage.isEmpty{
messages.append(Message(content: newMessage, isCurrentUser: true))
messages.append(Message(content: "Reply of " + newMessage , isCurrentUser: false))
newMessage = ""
}
}

To replicate a real chat app, we will append the message in the Message array twice. One as a reply to the main message.

Also, if you notice any real app, you will see that last message will show first. I mean it will scroll from the bottom. For that, we have to use ScrollViewReader. When we open the app, it will help to scroll to the bottom of the list. Also if we append a new item in the list, it will help to scroll down. Here is the final code of ContentView.Swift:

import Combine
import SwiftUI

struct ContentView: View {
@State var messages = DataSource.messages
@State var newMessage: String = ""


var body: some View {

VStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
List(messages, id: \.self) { message in
MessageView(currentMessage: message)
.id(message)
}
}
.onReceive(Just(messages)) { _ in
withAnimation {
proxy.scrollTo(messages.last, anchor: .bottom)
}

}.onAppear {
withAnimation {
proxy.scrollTo(messages.last, anchor: .bottom)
}
}
}

// send new message
HStack {
TextField("Send a message", text: $newMessage)
.textFieldStyle(.roundedBorder)
Button(action: sendMessage) {
Image(systemName: "paperplane")
}
}
.padding()
}
}



}

func sendMessage() {

if !newMessage.isEmpty{
messages.append(Message(content: newMessage, isCurrentUser: true))
messages.append(Message(content: "Reply of " + newMessage , isCurrentUser: false))
newMessage = ""
}
}
}

#Preview {
ContentView()
}

You can get the project from GitHub.

--

--