Custom Message List

The beauty of SwiftUI is that it’s easy to mix and match components. You can use the built-in components as a way to quickly build in-app chat. Stream exposes a full low level client with offline storage support. So it’s also easy to build any UI component on top of that.

To showcase the usage of our low-level client, we will build a simple livestream chat view, similar to Twitch.

Screenshot of the custom message list.

The state for a given channel is available via the ChatChannelController. You can create a channel controller using the ChatClient for a given channel id. In this example, for simplicity we will use a hardcoded channel id. If you want to listen to the changes of the channel controller (for example messages changes), you need to implement methods from the ChatChannelControllerDelegate or listen to the Combine publisher changes.

To make the usage easier from SwiftUI views, we will create a view model that will manage the state and listen for the channel controller state updates.

import Combine
import StreamChat

class LivestreamChatViewModel: ObservableObject, ChatChannelControllerDelegate {
    
    @Published var messages = LazyCachedMapCollection<ChatMessage>()
    @Published var text = ""
    
    private let channelController: ChatChannelController
    private var loadingPreviousMessages = false
    private var cancellables = Set<AnyCancellable>()
    
    init(chatClient: ChatClient) {
        channelController = chatClient.channelController(
            for: try! ChannelId(cid: "messaging:5A9427AD-E")
        )
        channelController.delegate = self
        channelController.synchronize { [weak self] error in
            guard let self else { return }
            self.messages = self.channelController.messages
        }
        channelController.messagesChangesPublisher.sink { [weak self] changes in
            guard let self else { return }
            self.messages = self.channelController.messages
        }
        .store(in: &cancellables)
    }
    
    func sendMessage() {
        channelController.createNewMessage(text: text)
        text = ""
    }
    
    func handleMessageAppear(index: Int) {
        if index >= messages.count {
            return
        }
        
        checkForPreviousMessages(index: index)
    }
    
    //MARK: - private
    
    private func checkForPreviousMessages(index: Int) {
        if index < messages.count - 25 {
            return
        }

        if !loadingPreviousMessages {
            loadingPreviousMessages = true
            channelController.loadPreviousMessages(
                before: nil,
                limit: 50,
                completion: { [weak self] _ in
                    guard let self = self else { return }
                    self.loadingPreviousMessages = false
                }
            )
        }
    }
}

The LivestreamChatViewModel in the code above, provides the list of messages, by listening to the updates of the messagesChangesPublisher from the channelController. You can find more details about the available Combine publishers here. Alternatively, you can also implement the delegate method didUpdateMessages to listen to these changes.

The LivestreamChatViewModel also provides a basic pagination support, by loading the previous messages when a message near the end of the current page appears on the screen. The channel controller’s loadPreviousMessages is responsible for loading the previous messages.

We also provide a way for the users to send a message. For simplicity, we only allow text messages in our example. However, you can easily provide more types of attachments, by either using our UI building blocks or creating your own views. The channelController’s method createNewMessage needs to be used for sending messages.

Next, let’s see how the UI implementation would look like:

import StreamChat
import SwiftUI

struct LivestreamChatView: View {
    
    @StateObject var viewModel: LivestreamChatViewModel
        
    init(chatClient: ChatClient) {
        let viewModel = LivestreamChatViewModel(chatClient: chatClient)
        _viewModel = StateObject(wrappedValue: viewModel)
    }
    
    var body: some View {
        ZStack {
            ScrollView {
                LazyVStack {
                    ForEach(viewModel.messages) { message in
                        if !message.text.isEmpty {
                            HStack {
                                Text(message.text)
                                    .font(.footnote)
                                    .bold()
                                Spacer()
                            }
                            .padding(.all, 4)
                            .onAppear {
                                let index = viewModel.messages.firstIndex { msg in
                                    msg.id == message.id
                                }
                                guard let index else { return }
                                viewModel.handleMessageAppear(index: index)
                            }
                        }
                    }
                }
                .padding()
            }
            
            VStack {
                Spacer()
                HStack {
                    TextField("Send a message", text: $viewModel.text)
                    Spacer()
                    Button(action: {
                        viewModel.sendMessage()
                    }, label: {
                        Text("Send")
                    })
                    .disabled(viewModel.text.isEmpty)
                }
                .padding()
                .background(Color(uiColor: .systemBackground))
            }
        }
    }
}

We are using a ScrollView with a LazyVStack to iterate through the messages and present the text. At the bottom, we also provide a simplified composer that consists of a text field and a button to send the message.

Finally, we just need to use the LivestreamChatView in our app, instead of the ChatChannelListView:

var body: some Scene {
    WindowGroup {
        LivestreamChatView(chatClient: chatClient)
    }
}

If you run the app now, you should see the following UI:

Screenshot of the custom message list.

This example shows how easy it is to build your own message list view, if you need a more custom messaging experience.

© Getstream.io, Inc. All Rights Reserved.