# iOS Native > iOS SDK Integration Guide - Voice-enabled AI agent with real-time communication capabilities (Swift / SwiftUI) URL: /embed/integration/ios Markdown: /embed/integration/ios.md # iOS Integration Guide [#ios-integration-guide] **SDK version:** 1.0.0\ **Platform:** iOS (Swift / SwiftUI)\ **Min deployment target:** iOS 16.0\ **Build system:** Xcode 15+ / Swift Package Manager Get your API key from [app.revrag.ai → Settings → API Keys](https://app.revrag.ai). *** ## Table of Contents [#table-of-contents] 1. [Requirements](#1-requirements) 2. [How it works](#2-how-it-works) 3. [Step 1 — Add the package](#step-1--add-the-package) 4. [Step 2 — Add microphone permission](#step-2--add-microphone-permission) 5. [Step 3 — Initialize the SDK](#step-3--initialize-the-sdk) 6. [Step 4 — Identify the user](#step-4--identify-the-user) 7. [Step 5 — Add the floating button](#step-5--add-the-floating-button) 8. [Routing scenarios](#routing-scenarios) 9. [Button visibility control](#button-visibility-control) 10. [Events](#events) 11. [Analytics helpers](#analytics-helpers) 12. [Cleanup on logout](#cleanup-on-logout) 13. [Configuration reference](#configuration-reference) 14. [Troubleshooting](#troubleshooting) 15. [Pre-ship checklist](#pre-ship-checklist) *** ## 1. Requirements [#1-requirements] | Requirement | Value | | ------------ | --------------- | | Min OS | iOS 16.0 | | Language | Swift 5.9+ | | UI framework | SwiftUI | | Build system | Xcode 15+ / SPM | *** ## Step 1 — Add the package [#step-1--add-the-package] ### Swift Package Manager (recommended) [#swift-package-manager-recommended] In Xcode: **File → Add Package Dependencies** Paste the URL: ``` https://github.com/RevRag-ai/embed-native ``` Select `Up to Next Major Version` from `1.0.0` → select `RevragEmbed` → click **Add Package**. ### CocoaPods alternative [#cocoapods-alternative] ```ruby # Podfile pod 'RevragEmbed', '~> 1.0' ``` ```bash pod install open YourApp.xcworkspace ``` *** ## Step 2 — Add microphone permission [#step-2--add-microphone-permission] **Required — your app will crash at runtime without this.** Without `NSMicrophoneUsageDescription`, iOS terminates the process the moment the SDK requests microphone access. This step cannot be skipped. Add to `Info.plist`: ```xml NSMicrophoneUsageDescription This app uses the microphone to talk with the AI agent. ``` **In Xcode:** Target → Info tab → `+` button → `Privacy - Microphone Usage Description` → enter a description. *** ## Step 3 — Initialize the SDK [#step-3--initialize-the-sdk] Call `EmbedSDK.shared.initialize()` **once**, as early as possible. ### SwiftUI App entry point [#swiftui-app-entry-point] ```swift // YourApp.swift import SwiftUI import RevragEmbed @main struct YourApp: App { init() { Task { await EmbedSDK.shared.initialize(apiKey: "YOUR_REVRAG_API_KEY") } } var body: some Scene { WindowGroup { ContentView() } } } ``` ### UIKit AppDelegate [#uikit-appdelegate] ```swift // AppDelegate.swift import UIKit import RevragEmbed @main class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { Task { await EmbedSDK.shared.initialize(apiKey: "YOUR_REVRAG_API_KEY") } return true } } ``` **What `initialize()` does:** 1. Calls `GET /embedded-agent/initialize` with your API key 2. Parses and stores your widget configuration (colors, agent name, avatar) 3. Pre-warms the Lottie animation cache 4. Installs `ClickEventTracker` for automatic rage-click detection 5. Sets `EmbedSDK.shared.isInitialized = true` on the main thread — the button appears automatically once true Console logs are prefixed `[RevragEmbed]` — check them if initialization fails. *** ## Step 4 — Identify the user [#step-4--identify-the-user] Call this right after your login flow completes: ```swift EmbedSDK.shared.event(.userData, data: [ "app_user_id": "user_123", // required "name": "Jane Doe", // optional "email": "jane@email.com" // optional ]) ``` Send `USER_DATA` **before** the user taps the call button. Without `app_user_id`, the agent cannot identify the user and conversation context will not be attributed. *** ## Step 5 — Add the floating button [#step-5--add-the-floating-button] Apply the `.embedProvider()` modifier to your **root view**. This overlays the draggable button on top of all your existing content. ```swift // ContentView.swift import SwiftUI import RevragEmbed struct ContentView: View { var body: some View { NavigationStack { HomeView() } .embedProvider( appUserId: "user_123", appVersion: Bundle.main.releaseVersionNumber ?? "1.0" ) } } ``` ### UIKit alternative [#uikit-alternative] Add `EmbedButton` as an overlay view in your root `UIViewController`: ```swift import RevragEmbed import SwiftUI // In viewDidLoad of your root UIViewController let embedView = UIHostingController(rootView: EmbedButton(appUserId: "user_123") ) embedView.view.backgroundColor = .clear addChild(embedView) view.addSubview(embedView.view) embedView.view.frame = view.bounds embedView.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] embedView.didMove(toParent: self) ``` ### `.embedProvider()` modifier props [#embedprovider-modifier-props] | Parameter | Type | Default | Description | | ---------------------- | ----------------------------- | --------------- | --------------------------------------- | | `appUserId` | `String` | `""` | Identifies the logged-in user | | `appVersion` | `String` | `"1.0.0"` | Your app's version string | | `accentColor` | `Color` | `#6C63FF` | Fallback gradient color | | `visibilityConfig` | `EmbedButtonVisibilityConfig` | show everywhere | Screen allow/exclude list and groups | | `navigationController` | `UINavigationController?` | `nil` | Enables automatic UIKit screen tracking | *** ## Routing scenarios [#routing-scenarios] Screen tracking lets the agent know which screen the user is on. Choose the scenario that matches your navigation setup. *** ### Scenario A — NavigationStack (most common) [#scenario-a--navigationstack-most-common] Post `EmbedViewDidAppear` from `.onAppear` on each screen. The SDK listens for this notification and updates the current screen name. ```swift struct HomeView: View { var body: some View { List { /* content */ } .onAppear { NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidAppear"), object: nil, userInfo: ["screen": "Home"] ) } } } struct ProductView: View { let productId: String var body: some View { ScrollView { /* content */ } .onAppear { NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidAppear"), object: nil, userInfo: ["screen": "ProductDetail"] ) } } } ``` Apply the provider to the root: ```swift struct ContentView: View { var body: some View { NavigationStack { HomeView() } .embedProvider(appUserId: "user_123", appVersion: "1.0") } } ``` `.onAppear` fires again when navigating back to a screen — this is expected and correct behavior. *** ### Scenario B — TabView with multiple stacks [#scenario-b--tabview-with-multiple-stacks] Apply `.embedProvider()` **outside** the `TabView` so the button floats above all tabs. Each tab's screens post `EmbedViewDidAppear` from `.onAppear` as shown in Scenario A. ```swift struct ContentView: View { var body: some View { TabView { NavigationStack { HomeView() } .tabItem { Label("Home", systemImage: "house") } NavigationStack { ExploreView() } .tabItem { Label("Explore", systemImage: "magnifyingglass") } NavigationStack { ProfileView() } .tabItem { Label("Profile", systemImage: "person") } } // Provider goes here — outside TabView — so button floats over all tabs .embedProvider(appUserId: "user_123", appVersion: "1.0") } } ``` Do **not** place `.embedProvider()` inside a tab. If it's scoped to one tab the button will disappear when switching to other tabs. *** ### Scenario C — No NavigationStack (flat views / custom transitions) [#scenario-c--no-navigationstack-flat-views--custom-transitions] Fire the notification manually when your view becomes visible. You can also post `EmbedViewDidDisappear` on exit. ```swift struct CheckoutView: View { var body: some View { VStack { /* content */ } .onAppear { NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidAppear"), object: nil, userInfo: ["screen": "Checkout"] ) } .onDisappear { // Optional — fires SCREEN_STATE with action: "exit" NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidDisappear"), object: nil, userInfo: ["screen": "Checkout"] ) } } } ``` *** ### Scenario D — UIKit UINavigationController [#scenario-d--uikit-uinavigationcontroller] Pass the `UINavigationController` to `.embedProvider()` for automatic tracking: ```swift struct ContentView: View { let navController: UINavigationController var body: some View { UIKitNavigationWrapper(navController: navController) .embedProvider( appUserId: "user_123", appVersion: "1.0", navigationController: navController // ← automatic tracking ) } } ``` The SDK observes `UINavigationControllerDelegate` and fires `SCREEN_STATE` events automatically — no `.onAppear` notifications needed. *** ## Button visibility control [#button-visibility-control] By default the button shows on **every screen**. Use `EmbedButtonVisibilityConfig` to control this. ### Show only on specific screens [#show-only-on-specific-screens] ```swift ContentView() .embedProvider( appUserId: "user_123", appVersion: "1.0", visibilityConfig: EmbedButtonVisibilityConfig( allowedScreens: ["Home", "Product", "Cart"], excludedScreens: ["Login", "Splash", "Onboarding"] ) ) ``` ### Keep button visible across a flow (e.g. checkout) [#keep-button-visible-across-a-flow-eg-checkout] Use a group with `.continuous` continuity so the button doesn't flash between screens: ```swift visibilityConfig: EmbedButtonVisibilityConfig( groups: [ EmbedButtonGroupConfig( id: "checkout-flow", screens: ["Cart", "Payment", "Confirmation"], continuity: .continuous, delayMs: 0.5, delayPolicy: .oncePerGroupEntry ) ] ) ``` ### Visibility rules (evaluated in priority order) [#visibility-rules-evaluated-in-priority-order] ``` 1. Screen is in excludedScreens → always HIDDEN (strongest rule) 2. allowedScreens and groups are both empty → always VISIBLE 3. Screen is in allowedScreens or any group → VISIBLE 4. Otherwise → HIDDEN ``` *** ## Events [#events] ### EventKeys reference [#eventkeys-reference] | Key | Value | Auto-fired | Host-callable | Notes | | ---------------- | ---------------- | ------------------------- | ------------- | ----------------------------------------------------------- | | `.userData` | `user_data` | No | **Yes** | Send after login; must include `app_user_id` | | `.screenState` | `screen_state` | **Yes** — `EmbedProvider` | Yes | Fired on every screen enter and exit | | `.analyticsData` | `analytics_data` | **Yes** — SDK UI | **Yes** | SDK fires built-in events; host may also fire custom events | | `.customEvent` | `custom_event` | No | Yes | Free-form host-app events | ### Sending events [#sending-events] ```swift // User identity (required before first call) EmbedSDK.shared.event(.userData, data: ["app_user_id": "user_123"]) // Custom analytics event EmbedSDK.shared.event(.analyticsData, data: ["event_name": "checkout_started"]) // Manual screen state EmbedSDK.shared.event(.screenState, data: ["screen": "CheckoutScreen", "action": "enter"]) ``` ### Listen for agent call events [#listen-for-agent-call-events] ```swift import RevragEmbed class CallManager { private var connectedId: UUID? private var disconnectedId: UUID? func startListening() { connectedId = EmbedSDK.shared.onAgent(.agentConnected) { _ in // e.g. pause background audio print("Call started") } disconnectedId = EmbedSDK.shared.onAgent(.agentDisconnected) { payload in let duration = (payload["metadata"] as? [String: Any])?["callDuration"] as? Int ?? 0 print("Call lasted \(duration)s") } } func stopListening() { if let id = connectedId { EmbedSDK.shared.offAgent(.agentConnected, id: id) } if let id = disconnectedId { EmbedSDK.shared.offAgent(.agentDisconnected, id: id) } } } ``` ### Agent lifecycle events reference [#agent-lifecycle-events-reference] | Event | Payload | Use case | | ---------------------- | ----------------------------------------------------- | ------------------------------------- | | `.agentConnected` | `timestamp`, `metadata.callDuration: 0`, `server_url` | Pause background audio, start a timer | | `.agentDisconnected` | `timestamp`, `metadata.callDuration` (seconds) | Resume audio, log call length | | `.popupMessageVisible` | — | Track tooltip impressions | *** ## Analytics helpers [#analytics-helpers] These helpers fire `analyticsData` events with standardized payloads. ```swift // Track a custom event EmbedSDK.shared.trackEvent("product_viewed", properties: ["product_id": "SKU-123"]) // Track a form interaction EmbedSDK.shared.trackFormEvent( formId: "checkout_form", eventType: "submit", formData: ["step": "payment"] ) // Track a rage-click EmbedSDK.shared.trackRageClick( elementId: "btn_add_to_cart", coordinates: CGPoint(x: 200, y: 450), clickCount: 5, elementType: "UIButton" ) // Check microphone permission status EmbedSDK.shared.checkMicPermission { granted in if !granted { self.showMicEducationAlert() } } ``` ### Events auto-fired by the SDK [#events-auto-fired-by-the-sdk] | `event_name` | Trigger | | ----------------------------- | ------------------------------------------------------------------ | | `agent_tap_to_open` | User taps the collapsed FAB | | `agent_tap_to_close` | User taps avatar to close the card | | `agent_visible` | `EmbedProvider` show-delay completes | | `popup_message_visible` | Inactivity nudge fires the tooltip | | `gen_tool_triggered` | First data-channel message received | | `agent_conversation_started` | LiveKit `connect()` succeeds | | `microphone_permission_allow` | On every call start (`status: already_granted` or `newly_granted`) | *** ## Cleanup on logout [#cleanup-on-logout] Call this when the user logs out or switches accounts to prevent stale data from leaking into the next session. ```swift func onUserLogout() { EmbedSDK.shared.clearStorageCache() } ``` *** ## Configuration reference [#configuration-reference] ### `EmbedButtonVisibilityConfig` [#embedbuttonvisibilityconfig] | Field | Type | Default | Description | | ----------------- | -------------------------- | ------------------ | ----------------------------------------------- | | `allowedScreens` | `[String]` | `[]` (all screens) | Button shows only on these screens | | `excludedScreens` | `[String]` | `[]` | Button always hidden on these screens | | `showDelay` | `TimeInterval` s | `0` | Delay before button fades in on allowed screens | | `groups` | `[EmbedButtonGroupConfig]` | `[]` | Per-group override rules | | `defaultInset` | `EmbedButtonInset?` | SDK default | Default edge snap position | ### `EmbedButtonGroupConfig` [#embedbuttongroupconfig] | Field | Type | Default | Description | | ------------- | ------------------------ | ------------ | ------------------------------------------------------------------------- | | `id` | `String` | required | Unique group identifier | | `screens` | `[String]` | required | Screens that belong to this group | | `continuity` | `EmbedButtonContinuity` | `.perScreen` | `.continuous` keeps the button mounted across group screens without flash | | `inset` | `EmbedButtonInset?` | `nil` | Per-group snap position override | | `delayMs` | `TimeInterval` s | `0` | Show delay for this group | | `delayPolicy` | `EmbedButtonDelayPolicy` | `.perScreen` | Controls when the delay resets | ### `EmbedButtonDelayPolicy` [#embedbuttondelaypolicy] | Value | Behaviour | | -------------------- | -------------------------------------------------- | | `.perScreen` | Delay applies every time the screen becomes active | | `.oncePerGroupEntry` | Delay only on the first entry into a group | | `.oncePerAppSession` | Delay only on the very first view of the session | ### `EmbedButton` direct props [#embedbutton-direct-props] | Parameter | Type | Default | Description | | ------------- | ------------------ | ------------------- | ----------------------------------------- | | `appUserId` | `String` | `""` | User identifier passed to the voice agent | | `accentColor` | `Color` | `#6C63FF` | Fallback gradient | | `isVisible` | `Bool` | `true` | Hides the button without unmounting it | | `inset` | `EmbedButtonInset` | right=24, bottom=80 | Snap position from screen edges | *** ## Troubleshooting [#troubleshooting] ### App crashes on first call [#app-crashes-on-first-call] **Error:** ``` This app has crashed because it attempted to access privacy-sensitive data without a usage description. ``` **Fix:** Add `NSMicrophoneUsageDescription` to `Info.plist`. See [Step 2](#step-2--add-microphone-permission) above. *** ### The button never appears [#the-button-never-appears] **Cause A — `initialize()` not called or failed** Check the Xcode console for `[RevragEmbed]` prefixed logs: ``` [RevragEmbed] 🚀 Stage 1 — initialize() called [RevragEmbed] 🌐 Stage 3 — calling GET /embedded-agent/initialize … [RevragEmbed] ❌ Initialization failed: ``` Fix: verify your API key has no leading/trailing spaces and the device has internet. *** **Cause B — SDK not ready yet** `initialize()` is async. The button is hidden until `isInitialized = true` and appears automatically — no action required. To observe readiness: ```swift print(EmbedSDK.shared.isInitialized) // should be true ``` *** **Cause C — Screen excluded by visibility config** Screen names are **case-sensitive** and must match exactly what you post in `EmbedViewDidAppear` notifications. ```swift // What you post in .onAppear: userInfo: ["screen": "ProductDetail"] // Must match exactly in visibilityConfig: allowedScreens: ["Home", "ProductDetail"] // ✅ correct allowedScreens: ["Home", "productdetail"] // ❌ case mismatch allowedScreens: ["Home", "Product Detail"] // ❌ space mismatch ``` *** ### Button disappears when switching tabs [#button-disappears-when-switching-tabs] **Cause:** `.embedProvider()` is applied inside a tab instead of outside the `TabView`. ```swift // ❌ Wrong — provider scoped to one tab only TabView { HomeView() .embedProvider(appUserId: "user_123") // hidden on all other tabs .tabItem { ... } } // ✅ Correct — provider wraps the entire TabView TabView { HomeView().tabItem { ... } ProfileView().tabItem { ... } } .embedProvider(appUserId: "user_123") // visible on all tabs ``` *** ### Screen tracking not working (agent doesn't know current screen) [#screen-tracking-not-working-agent-doesnt-know-current-screen] **Cause:** `EmbedViewDidAppear` notification not posted from `.onAppear`. ```swift // ✅ Add this to every screen's .onAppear .onAppear { NotificationCenter.default.post( name: NSNotification.Name("EmbedViewDidAppear"), object: nil, userInfo: ["screen": "YourScreenName"] ) } ``` *** ### Background music doesn't resume after a call [#background-music-doesnt-resume-after-a-call] The SDK calls `AVAudioSession.setActive(false, options: .notifyOthersOnDeactivation)` automatically in `endCall()`. If you observe this issue, ensure you are on SDK version ≥ 1.0 and that the session is not being deactivated before the SDK finishes cleaning up. *** ### Analytics events missing from dashboard [#analytics-events-missing-from-dashboard] Check in this order: 1. `USER_DATA` was sent with a valid `app_user_id` before other events 2. `initialize()` completed successfully (`isInitialized == true`) 3. Device has internet connectivity 4. The SDK rate-limits to 5 req/s — bursts are **queued, not dropped** *** ### User identity leaks between accounts [#user-identity-leaks-between-accounts] Always call `clearStorageCache()` on logout before the next user logs in: ```swift func onUserLogout() { EmbedSDK.shared.clearStorageCache() } ``` *** ## Pre-ship checklist [#pre-ship-checklist] ### Basic setup [#basic-setup] * [ ] Package added via Xcode SPM: `https://github.com/RevRag-ai/embed-native` * [ ] `NSMicrophoneUsageDescription` added to `Info.plist` * [ ] `await EmbedSDK.shared.initialize(apiKey:)` called in `App.init()` or `AppDelegate` * [ ] `USER_DATA` event sent with `app_user_id` immediately after login * [ ] `.embedProvider()` applied to the root view (outside `TabView` if tabs are used) * [ ] `clearStorageCache()` called on logout ### Screen tracking [#screen-tracking] * [ ] `EmbedViewDidAppear` notification posted from every screen's `.onAppear` (SwiftUI NavigationStack) * [ ] Or `navigationController` passed to `.embedProvider()` for automatic UIKit tracking * [ ] Screen names in notifications match `visibilityConfig` exactly (case-sensitive) ### Visibility [#visibility] * [ ] `visibilityConfig` configured if the button should not show on all screens * [ ] `.embedProvider()` placed outside `TabView` (if applicable) ### Production readiness [#production-readiness] * [ ] `[RevragEmbed]` logs checked — no initialization errors * [ ] Agent event listeners started and stopped at appropriate lifecycle points * [ ] Tested on a physical device — microphone permission dialog does not appear in Simulator *** ## Support [#support] * Issues: [GitHub Issues](https://github.com/RevRag-ai/embed-native/issues) * Docs: [Revrag Documentation](https://docs.revrag.ai) * Dashboard: [app.revrag.ai](https://app.revrag.ai)