Appearance
State Layer
About 1270 wordsAbout 4 min
Swift SDK - Chat API
Table of Contents
- ChatChannelController - Setup & Observation
- ChatChannelListController - Setup & Observation
- Message List Observation
- Typing Indicators
- Read State & Unread Counts
- Real-time Events
- SwiftUI - Channel List
- SwiftUI - Message List
- SwiftUI - Unread Count Badge
ChatChannelController - Setup & Observation
Create and observe a ChatChannelController for a specific channel. The controller provides reactive access to channel data, messages, and members. Set a delegate to receive callbacks when channel state changes.
Example
import StreamChat
let config = ChatClientConfig(apiKeyString: "YOUR_API_KEY")
let client = ChatClient(config: config)
client.connectUser(
userInfo: UserInfo(id: "john"),
token: .init(stringLiteral: "<user_token>")
)
// Create a channel controller
let channelId = ChannelId(type: .messaging, id: "general")
let channelController = client.channelController(for: channelId)
// Synchronize to fetch latest data
channelController.synchronize { error in
guard error == nil else { return }
// Access channel data
let channel = channelController.channel
print("Channel: \(channel?.name ?? "")")
print("Members: \(channel?.memberCount ?? 0)")
print("Messages: \(channelController.messages.count)")
}ChatChannelListController - Setup & Observation
Create and observe a ChatChannelListController to query and monitor a list of channels. The controller automatically updates when channels are added, removed, or modified.
Example
import StreamChat
let config = ChatClientConfig(apiKeyString: "YOUR_API_KEY")
let client = ChatClient(config: config)
client.connectUser(
userInfo: UserInfo(id: "john"),
token: .init(stringLiteral: "<user_token>")
)
// Create a channel list controller with a filter
let filter = Filter<ChannelListFilterScope>.containMembers(userIds: ["john"])
let sort: [Sorting<ChannelListSortingKey>] = [.init(key: .lastMessageAt, isAscending: false)]
let channelListController = client.channelListController(
query: ChannelListQuery(filter: filter, sort: sort)
)
// Synchronize to fetch channels
channelListController.synchronize { error in
guard error == nil else { return }
let channels = channelListController.channels
for channel in channels {
print("\(channel.name ?? channel.cid.description): \(channel.unreadCount.messages) unread")
}
}Message List Observation
Observe messages in a channel using the ChatChannelController. Messages are automatically sorted and updated in real time as new messages arrive or existing messages are modified.
Example
import UIKit
import StreamChat
class MessageListViewController: UIViewController, ChatChannelControllerDelegate {
let channelController: ChatChannelController
init(client: ChatClient, channelId: ChannelId) {
self.channelController = client.channelController(for: channelId)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
channelController.delegate = self
channelController.synchronize()
}
// Called when messages change
func channelController(
_ channelController: ChatChannelController,
didUpdateMessages changes: [ListChange<ChatMessage>]
) {
for change in changes {
switch change {
case .insert(let message, index: let index):
print("New message at \(index): \(message.text)")
case .update(let message, index: let index):
print("Updated message at \(index): \(message.text)")
case .remove(_, index: let index):
print("Removed message at \(index)")
case .move(_, fromIndex: let from, toIndex: let to):
print("Moved message from \(from) to \(to)")
}
}
}
}Typing Indicators
Observe and send typing indicators in a channel. Use sendKeystrokeEvent() to notify others that the current user is typing, and observe typing events through the channel controller delegate.
Example
import UIKit
import StreamChat
class TypingViewController: UIViewController, ChatChannelControllerDelegate {
let channelController: ChatChannelController
init(client: ChatClient, channelId: ChannelId) {
self.channelController = client.channelController(for: channelId)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError() }
override func viewDidLoad() {
super.viewDidLoad()
channelController.delegate = self
channelController.synchronize()
}
// Send a typing indicator when the user types
func textFieldDidChangeSelection(_ textField: UITextField) {
channelController.sendKeystrokeEvent { error in
if let error = error {
print("Failed to send typing event: \(error)")
}
}
}
// Send stop typing when the user stops
func textFieldDidEndEditing(_ textField: UITextField) {
channelController.sendStopTypingEvent { error in
if let error = error {
print("Failed to send stop typing event: \(error)")
}
}
}
// Observe who is currently typing
func channelController(
_ channelController: ChatChannelController,
didChangeTypingUsers typingUsers: Set<ChatUser>
) {
let names = typingUsers.map(\.name).compactMap { $0 }
if names.isEmpty {
print("No one is typing")
} else {
print("\(names.joined(separator: ", ")) typing...")
}
}
}Read State & Unread Counts
Observe read state and unread counts for channels. Use the channel controller to access per-channel unread counts and the current user controller for total unread counts across all channels.
Example
import StreamChat
let config = ChatClientConfig(apiKeyString: "YOUR_API_KEY")
let client = ChatClient(config: config)
client.connectUser(
userInfo: UserInfo(id: "john"),
token: .init(stringLiteral: "<user_token>")
)
// Per-channel unread count
let channelId = ChannelId(type: .messaging, id: "general")
let channelController = client.channelController(for: channelId)
channelController.synchronize { error in
guard error == nil else { return }
let unreadCount = channelController.channel?.unreadCount
print("Unread messages: \(unreadCount?.messages ?? 0)")
print("Unread mentions: \(unreadCount?.mentions ?? 0)")
}
// Mark channel as read
channelController.markRead { error in
if let error = error {
print("Failed to mark as read: \(error)")
}
}
// Total unread count across all channels
let currentUserController = client.currentUserController()
currentUserController.synchronize { error in
guard error == nil else { return }
let totalUnread = currentUserController.unreadCount
print("Total unread messages: \(totalUnread.messages)")
print("Total unread channels: \(totalUnread.channels)")
}Real-time Events
Subscribe to real-time events on a channel. The channel controller automatically receives events like new messages, reactions, member changes, and more. Use the delegate to handle specific event types.
Example
import StreamChat
class EventHandler: ChatChannelControllerDelegate {
let channelController: ChatChannelController
init(client: ChatClient, channelId: ChannelId) {
self.channelController = client.channelController(for: channelId)
channelController.delegate = self
channelController.synchronize()
}
// Called when the channel is updated (name, image, etc.)
func channelController(
_ channelController: ChatChannelController,
didUpdateChannel channel: EntityChange<ChatChannel>
) {
switch channel {
case .update(let channel):
print("Channel updated: \(channel.name ?? "")")
default:
break
}
}
// Called when members change
func channelController(
_ channelController: ChatChannelController,
didChangeMembers members: [ListChange<ChatChannelMember>]
) {
for change in members {
switch change {
case .insert(let member, index: _):
print("Member joined: \(member.name ?? member.id)")
case .remove(let member, index: _):
print("Member left: \(member.name ?? member.id)")
default:
break
}
}
}
// Called for new messages
func channelController(
_ channelController: ChatChannelController,
didUpdateMessages changes: [ListChange<ChatMessage>]
) {
for change in changes {
if case .insert(let message, index: _) = change {
print("New message from \(message.author.name ?? ""): \(message.text)")
}
}
}
}SwiftUI - Channel List
Display a list of channels in SwiftUI using ChatChannelListController wrapped in an ObservableObject. The view updates automatically as channels change.
Example
import SwiftUI
import StreamChat
import StreamChatUI
struct ChannelListView: View {
@StateObject var viewModel: ChannelListViewModel
init(client: ChatClient) {
let filter = Filter<ChannelListFilterScope>.containMembers(userIds: [client.currentUserId!])
let sort: [Sorting<ChannelListSortingKey>] = [.init(key: .lastMessageAt, isAscending: false)]
_viewModel = StateObject(wrappedValue: ChannelListViewModel(
client: client, filter: filter, sort: sort
))
}
var body: some View {
List(viewModel.channels, id: \.cid) { channel in
NavigationLink(destination: Text(channel.cid.description)) {
HStack {
VStack(alignment: .leading) {
Text(channel.name ?? channel.cid.description)
.font(.headline)
if let latestMessage = channel.latestMessages.first {
Text(latestMessage.text)
.font(.subheadline)
.foregroundColor(.secondary)
.lineLimit(1)
}
}
Spacer()
if channel.unreadCount.messages > 0 {
Text("\(channel.unreadCount.messages)")
.font(.caption)
.padding(6)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
}
}
}
}
class ChannelListViewModel: ObservableObject {
@Published var channels: [ChatChannel] = []
private let controller: ChatChannelListController
init(client: ChatClient, filter: Filter<ChannelListFilterScope>, sort: [Sorting<ChannelListSortingKey>]) {
self.controller = client.channelListController(
query: ChannelListQuery(filter: filter, sort: sort)
)
controller.synchronize { [weak self] _ in
self?.channels = Array(self?.controller.channels ?? [])
}
}
}SwiftUI - Message List
Display messages in a channel using SwiftUI with a ChatChannelController. The view updates in real time as new messages arrive.
Example
import SwiftUI
import StreamChat
import StreamChatUI
struct MessageListView: View {
@StateObject var viewModel: MessageListViewModel
init(client: ChatClient, channelId: ChannelId) {
_viewModel = StateObject(wrappedValue: MessageListViewModel(
client: client, channelId: channelId
))
}
var body: some View {
ScrollView {
LazyVStack(alignment: .leading, spacing: 8) {
ForEach(viewModel.messages, id: \.id) { message in
HStack(alignment: .top) {
Text(message.author.name ?? message.author.id)
.font(.caption)
.fontWeight(.bold)
Text(message.text)
.font(.body)
}
.padding(.horizontal)
}
}
}
}
}
class MessageListViewModel: ObservableObject {
@Published var messages: [ChatMessage] = []
private let controller: ChatChannelController
init(client: ChatClient, channelId: ChannelId) {
self.controller = client.channelController(for: channelId)
controller.synchronize { [weak self] _ in
self?.messages = Array(self?.controller.messages ?? [])
}
}
}SwiftUI - Unread Count Badge
Display and observe the total unread count badge in SwiftUI. Use the CurrentChatUserController to reactively track unread messages and channels.
Example
import SwiftUI
import StreamChat
struct UnreadBadgeView: View {
@StateObject var viewModel: UnreadCountViewModel
init(client: ChatClient) {
_viewModel = StateObject(wrappedValue: UnreadCountViewModel(client: client))
}
var body: some View {
HStack {
Image(systemName: "message.fill")
Text("Messages")
Spacer()
if viewModel.unreadMessages > 0 {
Text("\(viewModel.unreadMessages)")
.font(.caption)
.padding(6)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
}
}
}
}
class UnreadCountViewModel: ObservableObject {
@Published var unreadMessages: Int = 0
@Published var unreadChannels: Int = 0
private let controller: ChatCurrentUserController
init(client: ChatClient) {
self.controller = client.currentUserController()
controller.synchronize { [weak self] _ in
self?.unreadMessages = self?.controller.unreadCount.messages ?? 0
self?.unreadChannels = self?.controller.unreadCount.channels ?? 0
}
}
}