SwiftUI basics – Chat App SwiftUI App Code Github

SwiftUI basics - Chat App SwiftUI App Code Github

In this beginner-friendly tutorial, we’ll create a straightforward chat app for both iOS and macOS. We’ll achieve this using SwiftUI and the Swift SDK from Stream Chat. While Stream offers convenient UIKit components that are ready to use, we’ll dive deeper and explore how to craft our own custom elements using SwiftUI’s powerful capabilities. By the end of this tutorial, you’ll have a solid understanding of how to build custom chat components tailored to your app’s unique needs.

Stream Chat SwiftUI Example – iOS/macOS chat app built with SwiftUI and Stream Chat

What you need

  • iOS 14+/macOS 10.15+
  • Xcode 12+
  • Stream account

Step 1: Set up the Xcode project

To start, let’s create a SwiftUI project in Xcode.

SwiftUI basics
SwiftUI tutorials

Step 2: Set up the Stream Chat dependency

To install the Stream Chat dependency, we’ll use CocoaPods. If you prefer Carthage or Swift Package Manager, they’re also supported.

In the folder where you saved the project, run pod init and add StreamChat to the Podfile. It should look similar to this:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'SwiftUIChat' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for SwiftUIChat
pod 'StreamChat', '~> 3.1'
end

After you do that, run pod install --repo-update, wait a bit for it to finish, and open the project via the .xcworkspace that was created.

Step 3: Configure the Stream Chat Dashboard

Sign up at GetStream.io, create the application, and make sure to select development instead of production.

stream chat app swiftui

To make things simple for now, let’s disable both auth checks and permission checks. Make sure to hit save. When your app is in production, you should keep these enabled.

stream chat app

For future reference, you can see the documentation about authentication and permissions here.

Now, save your credentials, as we’ll need them to power the chat in the app. Since we disabled auth and permissions, we’ll only really need the key for now, but in production, you’ll use the secret in your backend to create JWTs to allow users to interact with your app securely.

swiftui stream chat app

As you can see, I’ve blacked out my keys. You should make sure to keep your credentials safe.

Step 4: Configure Stream Chat SDK

Now that we’ve set up the project and Stream Chat dashboard let’s make a singleton for the chat client so we can access it anywhere in our code.

import StreamChat

extension ChatClient {
    static var shared: ChatClient!
}

Now, set the singleton with the Stream Chat App’s key. The didFinishLaunchingWithOptions function in AppDelegate.swift should look similar to the following snippet.

import UIKit
import StreamChatClient

...
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        /// 1: Create a `ChatClientConfig` with the API key.
        let config = ChatClientConfig(apiKeyString: "kvgjrn7nmuuj")

        /// 2: Set the shared `ChatClient` instance with the config and a temporary token provider.
        ChatClient.shared = ChatClient(config: config, tokenProvider: .anonymous)
        return true
    }
...

That will configure the ChatClient.shared instance, which we’ll use throughout this tutorial to make calls to the Stream Chat API and populate our SwiftUI views with real-time data.

Step 5: Create the Login View

Finally, we can start using the power of SwiftUI to build the login screen, which will look similar to the following screenshot.

login chat swiftui

Let’s create a LoginView.swift file and paste the following contents.

import SwiftUI
import StreamChat

struct LoginView: View {
    @State
    private var username: String = ""
    @State
    private var success: Bool?
    
    var body: some View {
        VStack {
            TextField("Type username", text: $username).padding()
            NavigationLink(destination: ChatView(), tag: true, selection: $success) {
                EmptyView()
            }
            Button(action: logIn) {
                Text("Log in")
            }
        }
        .frame(maxHeight: .infinity, alignment: .top)
        .navigationBarTitle("Log in", displayMode: .inline)
    }
    
    func logIn() {
        /// 1: Set the chat client's token provider
        ChatClient.shared.tokenProvider = .development(userId: username)
        
        /// 2: Reload the current user
        ChatClient.shared.currentUserController().reloadUserIfNeeded { error in
            switch error {
            case .none:
                self.success = true
            case .some:
                self.success = false
            }
        }
    }
}

In that snippet of code, we create a TextField and a Button. The Button sets the ChatClient‘s tokenProvider to a development token and user id equal to the contents of the TextField for the username. After that, currentUserController().reloadUserIfNeeded is called to refresh the client with the new user. The callback to that function, if successful, will trigger the NavigationLink to the ChatView we’ll create in the next step.

Note we’re not collecting a password since we’re skipping authentication for this tutorial, so anyone can log in as the username they choose. Also, usernames may not contains spaces and some special characters. It would be best if you handled the possible errors.

Step 6: Create the Chat View

For this first part, we’ll have a single channel that the user joins as soon as they log in. It will look similar to the screenshot below. We’ll make it look a bit cozier in the next step.

chat swiftui

Let’s create a ChatView.swift file and paste the contents below.

import SwiftUI
import StreamChat

struct ChatView: View {
    @StateObject var channel = ChatClient.shared.channelController(
        for: ChannelId(
            type: .messaging,
            id: "general"
        )
    ).observableObject
    
    @State
    var text: String = ""
    
    var body: some View {
        VStack {
            List(channel.messages, id: \.self) {
                Text("\($0.author.id): \($0.text)")
                    .scaleEffect(x: 1, y: -1, anchor: .center)
            }
            .scaleEffect(x: 1, y: -1, anchor: .center)
            .offset(x: 0, y: 2)
            
            HStack {
                TextField("Type a message", text: $text)
                Button(action: self.send) {
                    Text("Send")
                }
            }.padding()
        }
        .navigationBarTitle("General")
        .onAppear(perform: { self.channel.controller.synchronize() })
    }
    
    func send() {
        channel.controller.createNewMessage(text: text) {
            switch $0 {
            case .success(let response):
                print(response)
            case .failure(let error):
                print(error)
            }
        }
        
        self.text = ""
    }
}

In that snippet, all we do is create a List for the messages and a simple composer with a TextField and a Button that sends the message with the function channel.controller.createNewMessage(text:). We also add a callback for the onAppear of this View, which synchronizes the local and remote data with channel.controller.synchronize(), which allows us to receive new messages in real-time. We now have a functioning chat, though the UI can be improved.

Step 7: Improving the message UI

Instead of that simple text cell, let’s create a custom view for it, similar to how it’s displayed in iMessage. It will look comparable to this screenshot.

swiftui chat app

Create MessageView.swift and paste the following contents.

import SwiftUI
import StreamChat

struct MessageView: View {
    let message: ChatMessage
    
    var background: some View {
        if (message.isSentByCurrentUser) {
            return Color.blue.opacity(0.25)
        } else {
            return Color.gray.opacity(0.25)
        }
    }
    
    var title: some View {
        if message.isSentByCurrentUser {
            return Text("")
        } else {
            return Text(message.author.id).font(.footnote)
        }
    }
    
    var body: some View {
        HStack {
            if message.isSentByCurrentUser { Spacer() }
            VStack(alignment: .leading) {
                title
                Text(message.text)
                .padding(8)
                .background(background)
                .cornerRadius(24)
            }
            if !message.isSentByCurrentUser { Spacer() }
        }.frame(maxWidth: .infinity)
    }
}

After that, replace the line Text("\($0.user.id): \($0.text)") in ChatView.swift with MessageView(message: $0).

To remove the separators between the messages, you’ll need to run this call before displaying the chat screen: UITableView.appearance().separatorStyle = .none. I suggest you put it in AppDelegate.swift, next to where you configure the Stream Chat Client.

Wrapping up

Congratulations! You’ve built the basis of a functioning chat app with SwiftUI and Stream. In part two, we’ll make a UI for creating and selecting channels. Stay tuned! Meanwhile, I encourage you to browse through Stream Chat’s docs and experiment with the project you just built.

Click here to go to part 2 of this tutorial which implements support for multiple channels.

Download – Github Code

SwiftUI basics – Chat App SwiftUI App Code Github

Related Posts

Leave a Reply

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