Adding the Mobile SDK to an iOS app

Last updated: 9 October 2026 · 10 min read

The iOS SDK uses system frameworks only and comes with a privacy manifest. Clomni sends iOS pushes straight to APNs with your .p8 key: iOS needs no Firebase.

Steps

  1. 1

    Requirements

    • iOS 15 or newer, Xcode 15 or newer.
    • The App ID and the iOS API key (ios_…) from the dashboard's Installation section.
    • For push: an Apple Developer account and an APNs key.
  2. 2

    Add the package

    Swift Package Manager: Xcode › File › Add Package Dependencies, enter https://github.com/clomni/clomni-mobile-sdk.git, choose “Up to Next Major Version” from 1.0.4 and add the ClomniMessenger product to the app target. In Package.swift:

    dependencies: [
        .package(url: "https://github.com/clomni/clomni-mobile-sdk.git", from: "1.0.4"),
    ],
    targets: [
        .target(name: "App", dependencies: [
            .product(name: "ClomniMessenger", package: "clomni-mobile-sdk"),
        ]),
    ]

    CocoaPods: the SDK is not on CocoaPods trunk, take the pod from the repository's tag. With pod 'ClomniMessenger', '1.0.4' CocoaPods cannot find the package.

    # Podfile, in your app's target
    pod 'ClomniMessenger', :git => 'https://github.com/clomni/clomni-mobile-sdk.git', :tag => '1.0.4'

    Users can send photos in the conversation. Add the permission texts to Info.plist:

    <key>NSPhotoLibraryUsageDescription</key>
    <string>To send photos to support</string>
    <key>NSCameraUsageDescription</key>
    <string>To take a photo and send it to support</string>
    <key>NSMicrophoneUsageDescription</key>
    <string>To send voice messages to support</string>

    Without NSCameraUsageDescription the Messenger does not offer the camera. Without NSMicrophoneUsageDescription the microphone button does not show (voice messages come with SDK 1.0.2).

  3. 3

    Start the SDK

    Call initialize once at launch. In a SwiftUI app, give it the AppDelegate with @UIApplicationDelegateAdaptor; push needs it anyway.

    import SwiftUI
    
    @main
    struct ExampleApp: App {
        @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
    
        var body: some Scene {
            WindowGroup { ContentView() }
        }
    }

    You can call the methods from any thread, callbacks arrive on the main thread. A second initialize is ignored. The full AppDelegate is in the push part below.

  4. 4

    User and Messenger

    let user = ClomniUser(userId: "12345", email: "[email protected]", name: "Aysel")
    Clomni.loginUser(user, userHash: hashFromYourServer)   // the hash comes from your server
    Clomni.updateUser(language: "az", customAttributes: ["plan": "premium"])
    
    // Together with your app's own logout:
    Clomni.logout()
    // SwiftUI
    Button("Support") { Clomni.present(source: "profile_support") }
    
    // UIKit
    @objc private func supportTapped() {
        Clomni.present(source: "profile_support")
    }

    The Messenger opens over the topmost screen. Call present after the screen is shown, not in didFinishLaunching. Other calls: presentNewConversation(source:), presentConversation(_:), dismiss(). The unread count: let token = Clomni.addUnreadCountListener { count in ... }, later Clomni.removeUnreadCountListener(token). The floating button: Clomni.setLauncherVisible(true), Clomni.setBottomPadding(72) (points).

    Call loginUser on every app start while the user is signed in, as if they had just signed in. For the same user the SDK reuses its stored session.

  5. 5

    Events, links, language and look

    To listen to the Messenger's events:

    Clomni.onMessengerOpened = { source in print("opened from \(source ?? "-")") }
    Clomni.onMessengerClosed = { print("closed") }
    Clomni.onConversationStarted = { id in print("conversation \(id)") }
    Clomni.onFlowCompleted = { flowId in print("flow \(flowId) completed") }
    Clomni.onUnreadCountChanged = { count in print("unread \(count)") }

    Each event has one listener; nil removes it.

    A news item's button can carry a web address or your app's deep link. With onLink set, every link comes to your app and the app opens it. Without it, the system opens links.

    Clomni.onLink = { url in
        if url.scheme == "example" {
            // route inside your app
        } else {
            UIApplication.shared.open(url)
        }
    }

    Language, sounds and your app's own colour:

    Clomni.setLanguage("en")          // az, en or ru; nil follows the phone
    Clomni.setSoundsEnabled(false)
    Clomni.setTheme(primaryColor: "#0A66C2", typeface: "Montserrat", mode: .dark)
    • setLanguage picks one of the languages turned on in the dashboard (Appearance › “Languages”). An empty value, or a language that is off, follows the phone's language, then the dashboard's main language.
    • Sounds: a short sound for a message sent or received while a conversation is open. It follows the phone's silent mode.
    • setTheme puts your app's own colour, font and mode over the dashboard's. The colour is #RRGGBB; the other brand colours are derived from it. Each call replaces the previous one, and a value left out stays the dashboard's. With a wrong format, the log says setTheme: primaryColor "…" is not #RRGGBB.
    • The font is the family name of a font in the app bundle, listed under UIAppFonts in Info.plist ("Montserrat" for Montserrat-Regular.ttf). Text keeps following Dynamic Type. For the font alone: Clomni.setTypeface("Montserrat"). When the font is not found, the log says font family "…" is not in the app; the system font stays and the system font is used.
  6. 6

    Push: capability

    Xcode › Project navigator › project › TARGETS › app › Signing & Capabilities › + Capability › Push Notifications. The configuration should be “All”.

  7. 7

    Push: AppDelegate

    import ClomniMessenger
    import UIKit
    import UserNotifications
    
    final class AppDelegate: NSObject, UIApplicationDelegate, @preconcurrency UNUserNotificationCenterDelegate {
        func application(_ application: UIApplication,
                         didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
            Clomni.initialize(appId: "app_…", apiKey: "ios_…")
            UNUserNotificationCenter.current().delegate = self
            Task {
                let center = UNUserNotificationCenter.current()
                let granted = try? await center.requestAuthorization(options: [.alert, .sound, .badge])
                if granted == true { UIApplication.shared.registerForRemoteNotifications() }
            }
            return true
        }
    
        func application(_ application: UIApplication,
                         didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
            Clomni.setDeviceToken(deviceToken)
        }
    
        // A push while the app is open. Clomni pushes stay hidden while the Messenger is open.
        @MainActor
        func userNotificationCenter(
            _ center: UNUserNotificationCenter,
            willPresent notification: UNNotification
        ) async -> UNNotificationPresentationOptions {
            Clomni.shouldShowForeground(notification.request.content.userInfo) ? [.banner, .list, .sound] : []
        }
    
        // A tap: a Clomni push opens its conversation.
        @MainActor
        func userNotificationCenter(_ center: UNUserNotificationCenter,
                                    didReceive response: UNNotificationResponse) async {
            if Clomni.handlePush(response.notification.request.content.userInfo) { return }
            // your app's own push
        }
    }
    • Keep both delegate methods @MainActor, not nonisolated. Otherwise a tap on a notification stops the app with “Call must be made on main thread”.
    • Ask for the notification permission at a moment that suits your users. The sample asks at launch for brevity.
    • Clomni.isClomniPush(userInfo) tells whether a push is Clomni's.
  8. 8

    Sandbox and production

    The SDK reports each device's APNs environment itself: a build run from Xcode uses sandbox; TestFlight, the App Store and Ad Hoc use production. One .p8 key serves both, provided it is created for Sandbox & Production. A key limited to one environment gives BadEnvironmentKeyInToken in the other. The “Test push” list shows each device's environment.

    If you want the agent's photo in the notification, add a Notification Service Extension. Clomni sends every push with mutable-content: 1 and an avatar_url. The extension's ready code is in the iOS chapter of the full guide, and it does not need the Clomni SDK.

  9. 9

    Check it

    1. Run the app on a real iPhone (the simulator does not receive APNs pushes from Clomni), allow notifications and open the Messenger once.
    2. Dashboard › your channel › settings column › Push › “Test push” › choose the device › “Send”.

    Logs are in the Xcode console and Console.app: subsystem ai.clomni.messenger, category Clomni. Common problems:

    • A log line starting with api_key that says the key is wrong or not for this platform (server error invalid_api_key): use the ios_… key. It also happens when the dashboard's “iOS Bundle ID” differs from the app's.
    • no window to present the messenger from yet: call present after the screen is shown.
    • “Test push” says BadEnvironmentKeyInToken: make a Sandbox & Production key. DeviceTokenNotForTopic: the Bundle ID in the Push section is not the app's.
  10. 10

    Full guide

    • PDF: Clomni Mobile SDK guide, in English, Azerbaijani and Russian.
    • Online: iOS.
    • In the dashboard: “Guide” in the channel's settings column.

Document