SwiftUI Chat App update

In this post, I’m writing a new version of the old posts about a chat GUI built with SwiftUI. I’ve removed the deprecated APIs and used the new ones released with iOS 18.

We are creating these screens: a list of contacts, a list of chats, and a chat screen. Note the red notification number on the chats tab.

The contacts

struct ContentView: View {
    init () {
        let navBarAppearance = UINavigationBarAppearance()
        navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.red]
        navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.red]
        UINavigationBar.appearance().backgroundColor = .black
        UINavigationBar.appearance().standardAppearance = navBarAppearance
        UINavigationBar.appearance().compactAppearance = navBarAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
        UINavigationBar.appearance().tintColor = UIColor.red
    }
    
    var body: some View {
        TabView {
            Tab("Contacts", systemImage: "rectangle.stack.person.crop.fill") {
                NavigationStack {
                    ContactsView()
                        .navigationBarTitle("Contacts", displayMode: .inline)
                }
            }
            Tab("Chat", systemImage: "message.fill") {
                NavigationStack {
                    ChatsView()
                }
            }.badge(3)
            Tab("Settings", systemImage: "gear") {
                SettingsView()
            }
        }
    }
}

In the init, we initialize the background and foreground colors for the tab bar and navigation.

The body contains a neat way to define the TabView (refer here for more details https://nicoladefilippo.com/tabview-in-ios-18-and-xcode-16/). The Chat tab uses the badge property to display the red number, as shown in the example.

Chats Vew

struct ChatsView: View {
    var chats: Array<Chat> = [Chat(name: "Alice", image: "person", lastMessage: "Bye", timestamp: Date(), chatId: "1"), Chat(name: "Bob", image: "person", lastMessage: "See soon", timestamp: Date(), chatId: "2")
    ]
    
    init () {
        let navBarAppearance = UINavigationBarAppearance()
        navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.red]
        navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.red]
        UINavigationBar.appearance().backgroundColor = .black
        UINavigationBar.appearance().standardAppearance = navBarAppearance
        UINavigationBar.appearance().compactAppearance = navBarAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
        UINavigationBar.appearance().tintColor = UIColor.red
    }
    
    var body: some View {
        VStack{
            NavigationStack {
                List {
                    ForEach(chats) { chat in
                        NavigationLink(destination: ChattingView()){
                            HStack {
                                Image(systemName: "person")
                                .resizable()
                                .frame(width: 30, height: 30)
                                VStack(alignment: .leading) {
                                    HStack {
                                        Text(chat.name)
                                        Spacer()
                                        Text(timeFormat(date: chat.timestamp))
                                        .foregroundStyle(.gray)
                                        .font(.footnote)
                                    }
                                    Text(chat.lastMessage)
                                    .foregroundStyle(.gray)
                                    .font(.callout)
                                }
                            }
                        }
                    }
                }.scrollContentBackground(.hidden).background(Color.black)
            }
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.black)
        .navigationBarTitle("Chats", displayMode: .inline)
    }
}

In this view, we define an array of contacts that we display using a List and ForEach, where each row contains an image, the contact’s name, and the last chat time.

With .scrollContentBackground(.hidden).background(Color.black), we set the List background to black.

The VStack that contains everything has the background also set to black, ensuring the entire screen is black, even when the list doesn’t fill the entire screen.

Chat View

struct ChattingView: View {
    @State var chats: Array<Chat> =
    [Chat(name: "Alice", image: "person", lastMessage: "Bye", timestamp: Date(), chatId: "1"),
     Chat(name: "Bob", image: "person", lastMessage: "See soon", timestamp: Date(), chatId: "2")
    ]
    
    @State var writing: String = ""
    init () {
        let navBarAppearance = UINavigationBarAppearance()
        navBarAppearance.titleTextAttributes = [.foregroundColor: UIColor.red]
        navBarAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.red]
        UINavigationBar.appearance().backgroundColor = .black
        UINavigationBar.appearance().standardAppearance = navBarAppearance
        UINavigationBar.appearance().compactAppearance = navBarAppearance
        UINavigationBar.appearance().scrollEdgeAppearance = navBarAppearance
        UINavigationBar.appearance().tintColor = UIColor.red
    }
    
    var body: some View {
        VStack(spacing: 0) {
            // Chat
            Spacer()
            List {
                ForEach(chats) { chat in
                    if chat.name == "Bob" {
                        UserRowView(chat: chat)
                        
                    } else {
                        ChatRowView(chat: chat)
                    }
                }.listRowBackground(Color.clear)
                .listRowSeparator(.hidden)
            }.scrollContentBackground(.hidden)
            .navigationBarTitle("Chatting", displayMode: .inline)
            .onTapGesture {
                self.endEditing()
            }
            // Input
            HStack() {
                TextEditor(text: $writing)
                    .frame(minHeight: 0, maxHeight: 50)
                    .border(Color.gray)
                
                Button(action: {
                    chats.append(Chat(name: "Bob", image:"", lastMessage: writing, timestamp: Date(), chatId: ""))
                    writing = ""
                    self.endEditing()
                }) {
                    Image(systemName: "paperplane")
                }
                
            }.ignoresSafeArea(.keyboard, edges: .bottom)
            .padding()
        }.background(Color.black)
    }
    
    private func endEditing() {
    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to:nil, from:nil, for:nil)
    }
}

This view also contains the settings for the tab bar and navigation. For the list of chats, we hide the scroll indicator and the row separator. We have two types of rows: one for the user (Bob) and the other to show messages from other people. We’ll take a closer look at that below. Now, please pay attention to the endEditing function; it’s called to hide the keyboard when the message is sent or when we want to stop typing.

Take a look at the rows:

struct ChatRowView: View {
    var chat: Chat
    var body: some View {
        VStack (alignment: .trailing){
            HStack() {
                Image(systemName: chat.image)
                    .resizable()
                    .frame(width: 30, height: 30)
                    .padding()
                Text(chat.lastMessage)
                Spacer()
            }.frame(maxWidth: 200)
            Text(timeFormat(date: chat.timestamp))
                .padding(2)
                .font(.caption)
                .foregroundStyle(.white)
        }.background(Color.blue)
        .clipShape(RoundedRectangle(cornerRadius: 10))
    }
}

struct UserRowView: View {
    var chat: Chat
    var body: some View {
        HStack {
            Spacer()
            VStack (alignment: .trailing){
                HStack() {
                    Spacer()
                    HStack {
                        Spacer()
                        Text(chat.lastMessage)
                            .padding()
                    }
                }
                Text(timeFormat(date: chat.timestamp))
                    .padding(2)
                    .font(.caption)
                    .foregroundStyle(.white)
            }.background(Color.green)
            .frame(minWidth: 10, maxWidth: 200)
            .clipShape(RoundedRectangle(cornerRadius: 10))
        }
    }
}

func timeFormat(date: Date) -> String {
    let formatter = DateFormatter()
    formatter.dateFormat = "HH:mm"
    return formatter.string(from: date)
}

The rows don’t have anything special apart from the alignment, but they use a new API to create rounded corners. Instead of using the deprecated cornerRadius, we utilize clipShape with a RoundedRectangle that has a corner radius.

You can find the comple code here: https://github.com/niqt/EEnigma/tree/main

share this post with friends

Picture of Nicola De filippo

Nicola De filippo

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve

Leave a comment

Your email address will not be published. Required fields are marked *

Who I am

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve.

Follow Me Here

Get The Latest Updates

Periodically receive my super contents on coding and programming

join the family;)

Recent Posts