SwiftUI integration
ChatCenterUISDK is a UIKit facade: getChat() returns a UIViewController that you embed via UINavigationController or present. To use it in a SwiftUI app, you need two things:
- An SDK instance holder — an
ObservableObjectexposed via@EnvironmentObjector@StateObject. - A
UIViewControllerRepresentablewrapper that displays the chat screen inside aNavigationStackorsheet.
SDK instance holder
A single ChatCenterUISDK instance lives for the entire app lifetime. In SwiftUI it is convenient to keep it in a @StateObject on the root App object and propagate it via environmentObject:
import SwiftUI
import ChatCenterUI
final class ChatSDKHolder: ObservableObject {
let sdk: ChatCenterUISDK
init() {
let transport = ChatTransportConfig(cloudHost: "your-host.edna.io")
let chatConfig = ChatConfig(transportConfig: transport)
self.sdk = ChatCenterUISDK(
providerUid: "YOUR_PROVIDER_UID",
appMarker: nil, // if you use multiple apps under one providerUid
chatConfig: chatConfig,
loggerConfig: ChatLoggerConfig(logLevel: .all)
)
}
}
@main
struct MyApp: App {
@StateObject private var chatHolder = ChatSDKHolder()
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(chatHolder)
}
}
}
ChatCenterUISDKDelegate is declared as class-only (AnyObject), and sdk.delegate is stored as a weak reference — so only a class can be the delegate, not a struct and not a View. In a SwiftUI project, ChatSDKHolder itself is typically the delegate: it conforms to ChatCenterUISDKDelegate and assigns sdk.delegate = self in init.
Authorization and push permission requests
Authorize the user where the app first knows about them — for example, after a successful sign-in. In SwiftUI this is .task, onChange, or a manual call from a view model:
struct LoginCompletedView: View {
@EnvironmentObject var chatHolder: ChatSDKHolder
var body: some View {
Color.clear.task {
let user = ChatUser(identifier: "user_uuid", name: "Ivan Ivanov")
chatHolder.sdk.authorize(user: user)
// For JWT authorization, pass auth: ChatAuth(...) — see [User management](../methods/auth.md)
}
}
}
Push permissions and the device token are handled through AppDelegate (see the Passing the token section in the notifications guide). In a SwiftUI App project, use @UIApplicationDelegateAdaptor:
@main
struct MyApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var chatHolder = ChatSDKHolder()
var body: some Scene { ... }
}
UIViewControllerRepresentable wrapper
struct ChatScreen: UIViewControllerRepresentable {
let chatCenterSDK: ChatCenterUISDK
let userInfo: [AnyHashable: Any]?
init(chatCenterSDK: ChatCenterUISDK, userInfo: [AnyHashable: Any]? = nil) {
self.chatCenterSDK = chatCenterSDK
self.userInfo = userInfo
}
func makeUIViewController(context: Context) -> UIViewController {
do {
return try chatCenterSDK.getChat(userInfo: userInfo)
} catch {
// Return a placeholder: SwiftUI does not allow cancelling the presentation of a representable
let vc = UIViewController()
vc.view.backgroundColor = .systemBackground
// replace with your own error view
return vc
}
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
getChat() can throwYou cannot cancel the presentation of a representable from inside makeUIViewController once an error occurs. Make the decision to open the chat before creating ChatScreen: call getChat() synchronously in the view model and flip a @State flag to present it. See the recommended pattern below.
Recommended pattern: pre-flight presentation
Call getChat() before presenting the screen and store the result. The error is handled before SwiftUI starts rendering:
@MainActor
final class ChatLauncher: ObservableObject {
@Published var preparedController: UIViewController?
@Published var lastError: Error?
private let sdk: ChatCenterUISDK
init(sdk: ChatCenterUISDK) { self.sdk = sdk }
func openChat(userInfo: [AnyHashable: Any]? = nil) {
do {
preparedController = try sdk.getChat(userInfo: userInfo)
} catch {
lastError = error
}
}
}
struct RootView: View {
@EnvironmentObject var chatHolder: ChatSDKHolder
@StateObject private var launcher: ChatLauncher
init(chatHolder: ChatSDKHolder) {
_launcher = StateObject(wrappedValue: ChatLauncher(sdk: chatHolder.sdk))
}
var body: some View {
Button("Open chat") {
launcher.openChat()
}
.sheet(item: Binding(
get: { launcher.preparedController.map(IdentifiableVC.init) },
set: { _ in launcher.preparedController = nil }
)) { wrapper in
ChatViewControllerWrapper(viewController: wrapper.viewController)
.ignoresSafeArea()
}
.alert("Failed to open chat",
isPresented: Binding(
get: { launcher.lastError != nil },
set: { _ in launcher.lastError = nil }
)) {
Button("OK") { launcher.lastError = nil }
}
}
}
private struct IdentifiableVC: Identifiable {
let id = UUID()
let viewController: UIViewController
}
private struct ChatViewControllerWrapper: UIViewControllerRepresentable {
let viewController: UIViewController
func makeUIViewController(context: Context) -> UIViewController { viewController }
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
NavigationStack vs sheet
| Option | When to use |
|---|---|
.sheet(item:) | Chat as a modal over the main interface. Easy to dismiss with a swipe. |
NavigationStack + NavigationLink(value:) | Chat as part of the navigation stack, allowing further navigation. The chat controller gets its own navigationItem, title, and Back button. |
.fullScreenCover(item:) | Full screen without swipe-to-dismiss — for apps where chat is the primary screen. |
In the NavigationStack variant, use NavigationLink instead of pushViewController. The controller returned by getChat() manages its navigationBar via NavigationBarStyle, so hide the SwiftUI system navigationBar on this screen:
.toolbar(.hidden, for: .navigationBar)
Limitations
- Navigation customization is done through
ChatTheme.flows.chatFlow.navigationBarStyle, not via SwiftUI modifiers. Configure the title, color, and buttons inChatTheme, not in.navigationTitle. - Switching the theme (light↔dark) inside an open chat only takes effect on the next
getChat()call. To make the screen rebuild based on the SwiftUI environment'scolorScheme, recreate it. @MainActoron the delegate —ChatCenterUISDKDelegatemethods may arrive off the main thread (see SDK delegate). For Swift 6 compatibility, use@preconcurrencyor addDispatchQueue.main.asyncinside the delegate methods.
Related sections
- SDK delegate — event handling
- User management —
authorize,logout, switching users - Presenting the chat — UIKit variant
- Notifications — push and cold start