# iOS

> Add Kustomer Chat to an iOS app with the KustomerChat Swift SDK. Swift Package Manager and CocoaPods setup, Kustomer.setup, presenting the chat UI, the headless ChatProvider and KUSChatListener, identity, theming, localization, and the privacy manifest.

Source: https://developer.kustomer.com/docs/chat/ios

The iOS SDK is a single Swift module, `KustomerChat`. It contains both the prebuilt chat and knowledge base UI and the headless API underneath it, so every integration starts with `import KustomerChat`.

|              |                                                      |
| ------------ | ---------------------------------------------------- |
| SDK          | 8.3.0                                                |
| iOS          | 15.0+                                                |
| Swift        | Swift 6 toolchain                                    |
| Distribution | Swift Package Manager, CocoaPods, or the XCFramework |

Sample apps for SPM and CocoaPods: [`kustomer/ios-chat-sdk-samples`](https://github.com/kustomer/ios-chat-sdk-samples).

## Install

**Swift Package Manager**

In Xcode, choose **File › Add Package Dependencies…** and enter:

```text
https://github.com/kustomer/kustomer-ios-spm
```

Use **Up to Next Major Version** from `8.3.0` and add the `kustomer-ios` product to your app target. Or in `Package.swift`:

```swift title="Package.swift"
dependencies: [
  .package(url: "https://github.com/kustomer/kustomer-ios-spm", from: "8.3.0"),
],
targets: [
  .target(name: "MyApp", dependencies: [
    .product(name: "kustomer-ios", package: "kustomer-ios-spm"),
  ]),
]
```

The package is a prebuilt binary with no package dependencies to resolve.

**CocoaPods**

```ruby title="Podfile"
platform :ios, '15.0'
use_frameworks!

target 'MyApp' do
  pod 'KustomerChat', '~> 8.3.0'
end
```

```bash
pod install
```

**XCFramework**

Download `KustomerChat.xcframework` from [`kustomer/kustomer-ios`](https://github.com/kustomer/kustomer-ios), drag it into your project, and set it to **Embed & Sign**. It contains device and simulator slices.

> **Upgrading to 8.3:** 8.3.0 requires JWTDecode 4.0 when you install with CocoaPods, so pins on JWTDecode 3.x no longer resolve. It also drops SnapKit; if your app used SnapKit only because the SDK brought it in, add it yourself.

### Info.plist

Add usage descriptions so customers can send photos and videos:

| Key                              | Why                                        |
| -------------------------------- | ------------------------------------------ |
| `NSCameraUsageDescription`       | Taking photos or videos to attach          |
| `NSMicrophoneUsageDescription`   | Recording video with sound                 |
| `NSPhotoLibraryUsageDescription` | Recommended for attaching from the library |

Files are picked with the system document picker, which needs no permission.

## Set up

#### Get a chat key

Create an API key in **Settings › Security › API Keys** with **only** the `org.tracking` role and no expiration. Setup fails with any other roles.

#### Call setup at launch

```swift title="AppDelegate.swift"
import UIKit
import KustomerChat

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
  func application(_ application: UIApplication,
                   didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    let options = KustomerOptions()
    options.brandId = "YOUR_BRAND_ID"
    options.logLevels = [.error, .warning]

    _ = Kustomer.setup(apiKey: kustomerChatKey, options: options, launchOptions: launchOptions) { result in
      if case .failure(let error) = result {
        print("Kustomer setup failed: \(error)")
      }
    }
    return true
  }
}
```

- Call it from `application(_:didFinishLaunchingWithOptions:)`. In a SwiftUI app, use `@UIApplicationDelegateAdaptor`.
- `setup` returns the client, and the result isn't discardable, so assign it to `_`.
- Call it once. A second call fails with `alreadyConfigured`, and options can't be replaced after setup.

#### Open chat

```swift
Kustomer.show()
```

`Kustomer` is a global, so every call is `Kustomer.method(…)`. `Kustomer.configure(…)` still works but is deprecated: `setup` always calls its completion. Check progress with `Kustomer.status`, which goes from `uninitialized` to `configured`, `started` and `connected`.

## Options

`KustomerOptions` is a class; set properties before passing it to `setup`.

| Property                                        | Default         | Description                                                                         |
| ----------------------------------------------- | --------------- | ----------------------------------------------------------------------------------- |
| `brandId`                                       | Org default     | Brand whose chat settings and knowledge base to use                                 |
| `businessScheduleId`                            | Org default     | Business schedule used for availability                                             |
| `language`                                      | Device language | Force a `KustomerLanguage`, such as `.fr` or `.zh_CN`                               |
| `activeAssistant`                               | `.orgDefault`   | `.none`, `.orgDefault` or `.withId("…")`. UI only.                                  |
| `enableUI`                                      | `true`          | `false` for a headless integration. Also turns off in-app notifications.            |
| `showInAppNotifications`                        | `true`          | Banners for new messages while your app is in the foreground                        |
| `hideNewConversationButton`                     | `false`         | Never show **New conversation**                                                     |
| `hideNewConversationButtonInClosedChat`         | `false`         | Hide it only in ended conversations                                                 |
| `hideHistoryNavigation`                         | `false`         | Hide the back button to chat history                                                |
| `hideKbArticleShare`                            | `false`         | Hide sharing on knowledge base articles                                             |
| `showAllBrandsConversationHistory`              | `false`         | Show every brand's conversations in history                                         |
| `enableHistoryStatusFilter`                     | `false`         | Status filter menu on history: All, Open, Closed, Unread                            |
| `historyStatusFilter`                           | `.none`         | Initial filter: `.none`, `.open`, `.closed` or `.requiresAttention`                 |
| `enableUrlUnfurling`                            | `true`          | Link previews in messages                                                           |
| `showAnimationFor5StarCSATRatings`              | `false`         | Celebrate 5-star satisfaction ratings                                               |
| `maxChatAttachmentTotalMB`                      | 50              | Total attachment size per message, 0–50                                             |
| `messageAttachmentLocalCacheMaximumSizeInBytes` | 25 MB           | Attachment cache size. Minimum 10 MB.                                               |
| `disablePush`                                   | `false`         | Don't register for push. A device that's already registered keeps receiving pushes. |
| `pushEnvironment`                               | `.production`   | `.development` for debug builds                                                     |
| `pushBundleId`                                  | Your bundle ID  | Bundle ID to register push under                                                    |
| `logLevels`                                     | `[]` (off)      | Any of `.debug`, `.info`, `.warning`, `.error`; `KustomerLogTypeAll` for everything |
| `onShow`, `onDismiss`                           | `nil`           | Closures called when the chat UI appears and goes away                              |

Change some settings at runtime:

```swift
Kustomer.overrideBrandId("OTHER_BRAND_ID") { error in }
Kustomer.changeBusinessSchedule(scheduleId: "SCHEDULE_ID") { result in }
Kustomer.changeActiveAssistant(.withId("ASSISTANT_ID"))
Kustomer.setHistoryStatusFilter(.open)
```

## Present the UI

UI methods are safe to call from any thread; the SDK moves to the main thread itself.

```swift
Kustomer.show()                                              // follows the brand's widget setting
Kustomer.show(preferredView: .onlyChat)
Kustomer.show(preferredView: .knowledgeBase, presentingVC: self)

Kustomer.startNewConversation(
  initialMessage: KUSInitialMessage(body: "Where is my order?", direction: .user),
  customDescribe: ["orderNumberStr": "4417"],
  afterCreateConversationOrError: { result in
    if case .success(let conversation) = result { print(conversation.id) }
  }
)

Kustomer.openConversation(id: "6716a3a9c4e5f60012ab34c1") { result in }
Kustomer.openChatAssistant(id: "ASSISTANT_ID", startDialog: nil, completion: { result in })
Kustomer.showKbArticle(id: "ARTICLE_ID")
Kustomer.showKbCategory(id: "CATEGORY_ID")
Kustomer.close(completion: nil)
Kustomer.isVisible
```

| `KustomerDisplayMode`                  | Shows                                                     |
| -------------------------------------- | --------------------------------------------------------- |
| `.default`                             | The experience chosen in the brand's chat widget settings |
| `.chatHistory`                         | Chat history                                              |
| `.newChat`                             | A new conversation                                        |
| `.activeChat`                          | The most recent active conversation                       |
| `.knowledgeBase`, `.knowledgeBaseHome` | The knowledge base                                        |
| `.onlyChat`                            | Chat only, without the knowledge base tab                 |
| `.onlyKnowledgeBase`                   | Knowledge base only, without the chat tab                 |

- A `.user` initial message is sent and creates the conversation. A `.agent` one is shown as a greeting until the customer replies.
- `customDescribe` sets conversation custom attributes, using attribute names with type suffixes.
- In apps that use `UIScene`, pass `presentingVC:` so the SDK presents from the right window.
- `openChatAssistant` and `close` have parameters without defaults, so `startDialog:` and `completion:` are required, as above.

### Unread counts and availability

```swift
Kustomer.getUnreadCount { result in
  if case .success(let count) = result { badge.text = count > 0 ? "\(count)" : nil }
}

if Kustomer.isChatAvailable() == false {
  // chat is off, or the business schedule says it's closed
}
```

`isChatAvailable()` returns `nil` before chat settings have loaded, `false` when chat is off or outside business hours, and `true` otherwise. `getOpenConversationCount` counts conversations that aren't ended and had a message in the last 30 days.

## Headless: ChatProvider

For your own UI, set `options.enableUI = false` and use `Kustomer.chatProvider`:

```swift
let chat = Kustomer.chatProvider

chat.createConversation(firstCustomerMessage: "Where is my order?", title: "Order 4417") { result in
  switch result {
  case .success(let created):
    chat.sendChatMessage(text: "It was due Monday.", conversationId: created.conversation.id) { _ in }
  case .failure(let error):
    print(error)
  }
}
```

| Method                                                                         | Purpose                                                                                            |
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `getConversationPage(page:pageSize:completion:)`                               | The customer's conversations, paged. Prefer it over `getConversations`, which loads everything.    |
| `getConversation(conversationId:completion:)`                                  | One conversation                                                                                   |
| `getActiveConversations(completion:)`                                          | Conversations that haven't ended                                                                   |
| `createConversation(firstCustomerMessage:title:customDescribe:…:completion:)`  | Create a conversation. Returns the conversation, its messages and the customer.                    |
| `endConversation(conversationId:completion:)`                                  | End it                                                                                             |
| `getChatMessages(conversationId:completion:)`                                  | Messages, oldest first                                                                             |
| `getChatMessagesBefore(timestamp:conversationId:count:completion:)`            | Older messages, **newest first**                                                                   |
| `sendChatMessage(text:conversationId:completion:)`                             | Send text. Overloads send an image (`image:`), a file (`fileUrl:`) or a button action (`action:`). |
| `markSessionAsRead(sessionId:completion:)`                                     | Mark a conversation read                                                                           |
| `listenForTyping(conversationId:)` / `stopListeningForTyping(conversationId:)` | Agent typing indicators                                                                            |
| `sendTypingStatus(for:isTyping:)`                                              | The customer's typing indicator                                                                    |
| `sendPresenceActivity(_:)`                                                     | `.online` or `.offline`                                                                            |
| `describeConversation(conversationId:attributes:_:)`                           | Conversation custom attributes                                                                     |
| `reloadChatSettings(_:)`                                                       | Refresh chat settings. Don't call it on every launch.                                              |

With `enableUI = false`, start and stop the connection yourself with `Kustomer.start()` and `Kustomer.stop()`. Never call `stop()` when you use the SDK's UI.

The knowledge base is on `Kustomer.kbProvider`: `getRootCategory(lang:knowledgeBaseID:completion:)`, `getCategory(by:childrenLimit:lang:knowledgeBaseID:completion:)`, `searchArticles(term:page:pageSize:lang:completion:)` and `getArticle(by:lang:knowledgeBaseID:completion:)`. Satisfaction surveys are handled by the UI only.

### Listen for events

Conform to `KUSChatListener`. Every method has a default implementation, so implement only the ones you need, and keep the returned handle to remove the listener later.

```swift
final class ChatEvents: KUSChatListener {
  func onChatMessageReceived(conversationId: String, chatMessage: KUSChatMessage) { }
  func onUnreadCountChange(count: Int) { }
  func onUserIsTyping(conversationId: String, user: KUSUser, timetoken: KUSPNTtoken?, ai: Bool) { }
  func onUserStopsTyping(conversationId: String, user: KUSUser, timetoken: KUSPNTtoken?) { }
  func onConversationEnded(conversationId: String, conversation: KUSConversation) { }
}

let events = ChatEvents()
let handle = Kustomer.chatProvider.addChatListener(events)
// later
Kustomer.chatProvider.removeChatListener(handle)
```

> **Match signatures exactly:** Because every listener method has a default, a method with the wrong signature compiles but is never called. The typing callback includes `ai: Bool`; older docs left it out.

| Callback                                                                                          | Fires when                                                         |
| ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `onChatMessageReceived(conversationId:chatMessage:)`                                              | A message arrives. Order isn't guaranteed; sort by `createdAt`.    |
| `onUnreadCountChange(count:)`, `onUnreadCountChange(conversationId:count:)`                       | Unread counts change. Called on the main thread.                   |
| `onOpenConversationCountChange(count:)`                                                           | The open conversation count changes                                |
| `onUserJoined(conversationId:user:)`                                                              | An agent joins                                                     |
| `onUserIsTyping(…ai:)`, `onUserStopsTyping(…)`                                                    | Agent or AI typing starts and stops                                |
| `onPreviewChanged(conversationId:preview:previewDetails:)`                                        | The conversation preview text changes                              |
| `onConversationCreated`, `onConversationEnded`, `onConversationReOpened`, `onConversationDeleted` | Conversation lifecycle, each with `(conversationId:conversation:)` |
| `onConversationMerged(sourceConversation:targetConversation:)`                                    | Two conversations are merged                                       |
| `onAssistantEnded(conversation:)`                                                                 | An assistant finishes                                              |
| `onSatisfactionEventReceived(conversationId:satisfaction:)`                                       | A satisfaction survey is sent                                      |
| `onCustomerDeleted()`                                                                             | The customer was deleted                                           |
| `onSessionOpened`, `onSessionTitleChanged`, `onSessionCustomAttributesUpdated`                    | Conversation opened, renamed or updated                            |
| `onUrlPreviewReceived(conversationId:message:urlUnfurled:)`                                       | A link preview is ready                                            |

Connection state is on `ConnectionProvider.shared`: `status` is `connecting`, `connected`, `reconnecting`, `disconnected` or `disconnectedUnexpectedly`, and a `KUSConnectionListener` gets `onPubNubConnectionStatusChange(status:)`.

## Identity

```swift
if !Kustomer.isLoggedIn(userEmail: nil, userId: user.id) {
  Kustomer.logIn(jwt: tokenFromServer) { result in
    switch result {
    case .success(let identity): print("Chat customer", identity.customerId)
    case .failure(let error): print(error)
    }
  }
}

Kustomer.chatProvider.describeCurrentCustomer(sharedPhones: ["+15555550123"], custom: ["loyaltyTierStr": "gold"])

// on sign-out
Kustomer.logOut { error in }
```

- The identity is stored in the keychain, so check `isLoggedIn` instead of logging in on every launch. It compares `userId` with the token's `externalId` (case-insensitive), or `userEmail` with its `email`.
- A successful login clears the local conversation cache and rebuilds the UI. The completion runs on the main thread, and calling `Kustomer.show()` inside it is safe.
- `logOut` clears local state and deregisters push. It reports an error only if push deregistration failed, for example offline; local state is still cleared.
- `logOutThenLogIn(jwt:_:)` switches users in one call.
- In `describeCurrentCustomer`, use the `shared*` parameters; `email`, `phone`, `emails` and `phones` are deprecated.

The JWT comes from your server. See [Customer identity](https://developer.kustomer.com/docs/chat/identity).

## Push notifications

`setup` registers for remote notifications. Forward the device token from your `AppDelegate`:

```swift
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
  Kustomer.didRegisterForRemoteNotifications(deviceToken: deviceToken)
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
  Kustomer.didFailToRegisterForRemoteNotifications(error: error)
}
```

APNs keys, permission prompts and notification delegates are covered in [Push notifications](https://developer.kustomer.com/docs/chat/push-notifications#ios).

## Theming

Colors are customized by name, in either of two ways:

- **Asset catalog.** Add a color set named `KustomerColor.<name>` to your app's asset catalog, with Any and Dark appearances.
- **Code.** Assign `KustomerUI.Color.<name>` before calling `setup`.

```swift
KustomerUI.Color.outgoingMessageBubble = UIColor(named: "Brand")!
KustomerUI.Color.chatHeaderBackground = .systemBackground
```

| Area           | Color names                                                                                                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Messages       | `incomingMessageBubble`, `incomingMessageText`, `outgoingMessageBubble`, `outgoingMessageText`, `messagesBackground`, `messageUrlLinkOnAgentBubble`, `messageUrlLinkOnCustomerBubble`                   |
| Chat screen    | `chatHeaderBackground`, `chatHeaderTitle`, `chatHeaderText`, `chatInputBarBackground`, `chatTextInputBackground`, `sendChatMessageButton`, `chatNewConversationButton`, `chatNewConversationButtonText` |
| History        | `historyBackground`, `historyHeaderBackground`, `historyRowBackground`, `historyRowTitle`, `historyRowSubtitle`, `historyBadgeBackground`, `historyBadgeText`, `historyNewChatButton`                   |
| Knowledge base | `kbHeader`, `kbHeaderText`, `kbBrowseBackground`, `kbBrowseRowTitle`, `kbBrowseRowSubtitle`, `kbBrowseIconTint`                                                                                         |
| Navigation     | `navigationBarButton`, `tabBarBackground`, `tabBarItemColor`, `tabBarUnselectedItemColor`                                                                                                               |
| Buttons        | `quickActionButton`, `quickActionButtonText`, `quickActionButtonBorder`, `kobjectButton`, `kobjectButtonText`                                                                                           |
| Satisfaction   | `csatText`, `csatCommentBoxBackground`, `csatCommentBoxOutline`                                                                                                                                         |

The built-in colors support dark mode. Text uses Dynamic Type; there's no font API. On iOS 26 and later the UI uses the system's Liquid Glass styling, unless your app sets `UIDesignRequiresCompatibility` in `Info.plist`.

## Localization

The SDK follows the device language; set `options.language` to force one. To change any string, add a **localized** `Kustomer.strings` file (for example in `en.lproj`) with the keys you want to override:

```text title="en.lproj/Kustomer.strings"
"com.kustomer.week" = "week";
```

Run `Kustomer.printLocalizationKeys()` once to print every key. Strings are looked up in your localized `Kustomer.strings` first, then the SDK's own translation. A `Kustomer.strings` that isn't localized loses to the SDK's translations.

## Privacy

The SDK includes a privacy manifest (`PrivacyInfo.xcprivacy`). It declares required-reason APIs for disk space, file timestamps, user defaults and system boot time, declares no collected data types, and doesn't track. Xcode merges it into your app's privacy report.

## Troubleshooting

| Symptom                                                         | Fix                                         |
| --------------------------------------------------------------- | ------------------------------------------- |
| Setup fails: "API key must contain only the org.tracking role"  | Use a key with only `org.tracking`.         |
| `Result of call to 'setup' is unused` warning                   | Write `_ = Kustomer.setup(…)`.              |
| `alreadyConfigured`                                             | `setup` ran twice. Call it once, at launch. |
| Typing indicator callback never fires                           | Your method is missing `ai: Bool`.          |
| Chat opens behind other UI, or not at all, in a scene-based app | Pass `presentingVC:`.                       |
| String overrides ignored                                        | Localize `Kustomer.strings`.                |
| `Kustomer.close()` doesn't compile                              | Use `Kustomer.close(completion: nil)`.      |
