flowchart LR
app[Your app] --> swiftui[SwiftUI SDK]
app --> uikit[UIKit SDK]
app -. fully custom UI .-> llc
swiftui --> llc[Low-level client]
uikit --> llc
llc <--> api[Stream API]iOS Introduction
The iOS SDK enables you to build any type of chat or messaging experience for iOS. We provide a low-level client and two UI component SDKs, UIKit and SwiftUI, which give you a ready-made UI with a high level of customisation and reusability.
Start here
This SDK is the client half of your integration. App settings, permissions, webhooks, push templates and data retention are configured from your backend with a server-side SDK or the REST API, and the token your app connects with has to be signed there too. See the server-side overview for what belongs there.
How the SDK is structured
The low-level client (LLC) is the data layer: it manages offline state and handles API requests. The UIKit and SwiftUI SDKs sit on top of it and provide ready-made, highly customisable UI components built with the respective UI framework. Most apps integrate one of the UI SDKs; drop down to the low-level client when you want to build a fully custom UI. The walkthrough further down this page covers the basics of using the low-level client on its own.
Main features
-
UIKitandSwiftUISDKs use native patterns and paradigms from respective UI frameworks: The API follows the design of native system SDKs. It makes integration with your existing code easy and familiar. -
First-class support for
CombineandStructured Concurrency: Refer to getting started guides for Combine and Structured Concurrency. -
Offline support: Browse channels and send messages while offline.
-
Familiar behaviour : The UI elements are good platform citizens and behave like native elements; they respect
tintColor,layoutMargins, light/dark mode, dynamic font sizes, etc. -
Swift native API: Uses Swift's powerful language features to make the SDK usage easy and type-safe.
-
Fully open source implementation: You have access to the source code of our SDKs on GitHub: UIKit and SwiftUI.
-
Supports iOS 13+ and macOS 11+ We support iOS 13 and later, so your app can stay available to almost everyone. macOS 11 is also supported via Mac Catalyst.
Build with Stream CLI and Agent Skills
The Stream CLI and Agent Skills provide AI coding agents, such as Claude Code, Cursor, and Codex, with the tools and knowledge necessary to build apps with Stream.
Install the CLI and add the skills to your project:
curl -fsSL https://getstream.io/cli.sh | bash
getstream skills stream-swiftOnce installed, invoke it from your agent:
/stream Add Stream Chat to my SwiftUI app.The /stream skill acts as a router, reading your request and dispatching it to the specialist skill. You can also invoke the /stream-swift skill directly.
Stream Agent Skills can also be installed from skills.sh.
Installation
You can add Stream Chat to your Xcode project using Swift Package Manager, CocoaPods, and XCFrameworks. Please head over to our installation documentation here and start building a world class chat UI.
Chat client
Let's get started by initializing the client and setting the current user:
// Import StreamChat framework.
import StreamChat
// For simplicity, we extend ChatClient to add a static `shared` singleton
extension ChatClient {
static var shared: ChatClient!
}
// API key can be found on the dashboard: https://beta.dashboard.getstream.io/?product=chat
let config = ChatClientConfig(apiKey: .init("{{ api_key }}"))
let userID = "leia_organa"
// Create an instance of ChatClient and share it using the singleton
ChatClient.shared = ChatClient(config: config)
// Option A: with expiring token
// The `tokenProvider` closure will be called again when the token is expired
ChatClient.shared.connectUser(
userInfo: .init(id: userID),
tokenProvider: { providerResult in
loadChatToken(completion: providerResult)
},
completion: { error in
if let error = error {
print("Connection failed with: \(error)")
} else {
// User successfully connected
}
}
)
// or alternatively, using the async-await method
let connectedUser = try await ChatClient.shared.connectUser(
userInfo: .init(id: userID),
tokenProvider: { providerResult in
loadChatToken(completion: providerResult)
}
)
// An example of a token provider
func loadChatToken(completion: @escaping (Result<Token, Error>) -> Void) {
NetworkingLayer.getChatToken() { token in
do {
let token = try Token(rawValue: token)
completion(.success(token))
} catch {
completion(.failure(error))
}
}
}
// Option B: with a non-expiring token
// You can generate the token for this user from https://getstream.io/chat/docs/ios-swift/tokens_and_authentication/?language=swift
let token: Token = "{{ chat_user_token }}"
/// Connect the user using a closure based method
ChatClient.shared.connectUser(
userInfo: .init(id: userID),
token: token
) { error in
if let error = error {
print("Connection failed with: \(error)")
} else {
// User successfully connected
}
}
// or alternatively, using the async-await version
let connectedUser = try await ChatClient.shared.connectUser(
userInfo: .init(id: userID),
token: token
)The above snippet is for a mobile integration. Server-side API calls are a little different, but this is covered in detail later in the documentation.
Guest and anonymous users
In addition to connecting with a token, the SDK supports guest and anonymous connections:
// Connect as a guest user: the backend generates a token automatically
try await ChatClient.shared.connectGuestUser(userInfo: .init(id: "guest_user"))
// Connect as an anonymous user: no user identity is required
try await ChatClient.shared.connectAnonymousUser()Disconnecting and logging out
Use disconnect() to temporarily disconnect while preserving the local database, or logout() to fully sign out (which also removes the device's push notification token by default):
// Disconnect: keeps local data intact for offline use
await ChatClient.shared.disconnect()
// Logout: clears the session and removes the push device token
await ChatClient.shared.logout()
// Logout while keeping push notifications active (e.g. for silent pushes)
ChatClient.shared.logout(removeDevice: false) {
// Logout completed
}Channels
Let’s continue by initializing your first channel. A channel contains messages, a list of people that are watching the channel, and optionally a list of members (for private conversations). The example below shows how to set up a channel to support chat for a group conversation using completion handler based controllers and the async-await supported state-layer:
/// 1: Create a `ChannelId` that represents the channel you want to create.
let channelId = ChannelId(type: .messaging, id: "general")
/// 2: Use the `ChatClient` to create a `ChatChannelController` with the `ChannelId`.
let channelController = try chatClient.channelController(
createChannelWithId: channelId,
extraData: ["info": .string("Hello")]
)
/// 3: Call `ChatChannelController.synchronize` to create the channel.
channelController.synchronize { error in
if let error = error {
/// 4: Handle possible errors
print(error)
}
}/// 1: Create a `ChannelId` that represents the channel you want to create.
let channelId = ChannelId(type: .messaging, id: "general")
/// 2: Use the `ChatClient` to create a `Chat` with the `ChannelId`.
let chat = try chatClient.makeChat(
with: channelId,
extraData: ["info": .string("Hello")]
)
/// 3: Call `Chat.get(watch:)` to create the channel.
try await chat.get(watch: true)A ChannelId bundles the Channel Type and the Channel ID together (messaging and general in this case). You can also use channelController(createDirectMessageChannelWith:) (controller-based) or makeDirectMessageChat(with:) (state-layer) to create a direct message channel without an explicit channel ID. The backend generates the ID from the members. The channel type controls the settings used for this channel.
There are 5 default types of channels:
- livestream
- messaging
- team
- gaming
- commerce
These five options above provide you with the most sensible defaults for those use cases. You can also define custom channel types if Stream Chat defaults don’t work for your use-case.
The third argument is an object containing the channel data (extraData). You can add as many custom fields as you would like as long as the total size of the object is less than 5KB.
Messages
Now that we have the channel set up, let's send our first chat message:
channelController.createNewMessage(
text: "Hello",
extraData: ["info": .string("secret message")]
) { result in
switch result {
case .success(let messageId):
/// 2: Handle success
print(messageId)
case .failure(let error):
/// 3: Handle errors
print(error)
}
}let sentMessage = try await chat.sendMessage(
with: "Hello",
extraData: ["info": .string("secret message")]
)Similar to users and channels, the send method allows you to add custom fields. When you send a message to a channel, Stream Chat automatically broadcasts it to all the people that are watching this channel in real-time.
This is how you can listen to message list changes on the client-side:
//> import UIKit
//> import StreamChat
/// * Delegates *
class ViewController: UIViewController, ChatChannelControllerDelegate {
func channelController(
_ channelController: ChatChannelController,
didUpdateMessages changes: [ListChange<ChatMessage>]
) {
// animate the changes to the message list
}
}
let viewController = ViewController()
channelController.delegate = viewController// Observing message list changes using the state-layer
chat.state.$messages
.sink { messages in
// updated array of `messages`
}
.store(in: &cancellables)What's next
Now that you understand the building blocks of a fully functional chat integration, these are good places to go deeper:
FAQ
Can I try it without my own backend?
Yes. With the app in development mode and Disable Authentication Checks toggled in the dashboard, developer tokens let clients connect without a token service.
How long do tokens last?
Indefinitely, by default. For expiring tokens, pass a token provider instead of a static string.
Can users report messages?
Yes. Any user can flag a message or another user, and flagged content lands in the dashboard review queue. See moderation.