Skip to main content
Version: 5.9.0

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:

  1. An SDK instance holder — an ObservableObject exposed via @EnvironmentObject or @StateObject.
  2. A UIViewControllerRepresentable wrapper that displays the chat screen inside a NavigationStack or sheet.

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)
}
}
}
Where to assign the delegate

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 throw

You 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.

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) {}
}
OptionWhen 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 in ChatTheme, 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's colorScheme, recreate it.
  • @MainActor on the delegateChatCenterUISDKDelegate methods may arrive off the main thread (see SDK delegate). For Swift 6 compatibility, use @preconcurrency or add DispatchQueue.main.async inside the delegate methods.