Skip to main content

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.

Table of Contents

  1. Requirements
  2. How it works
  3. Step 1 — Add the package
  4. Step 2 — Add microphone permission
  5. Step 3 — Initialize the SDK
  6. Step 4 — Identify the user
  7. Step 5 — Add the floating button
  8. Routing scenarios
  9. Button visibility control
  10. Events
  11. Analytics helpers
  12. Cleanup on logout
  13. Configuration reference
  14. Troubleshooting
  15. Pre-ship checklist

1. Requirements

RequirementValue
Min OSiOS 16.0
LanguageSwift 5.9+
UI frameworkSwiftUI
Build systemXcode 15+ / SPM

Step 1 — Add the package

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

# Podfile
pod 'RevragEmbed', '~> 1.0'
pod install
open YourApp.xcworkspace

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:
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone to talk with the AI agent.</string>
In Xcode: Target → Info tab → + button → Privacy - Microphone Usage Description → enter a description.

Step 3 — Initialize the SDK

Call EmbedSDK.shared.initialize() once, as early as possible.

SwiftUI App entry point

// 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

// 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

Call this right after your login flow completes:
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

Apply the .embedProvider() modifier to your root view. This overlays the draggable button on top of all your existing content.
// 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

Add EmbedButton as an overlay view in your root UIViewController:
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

ParameterTypeDefaultDescription
appUserIdString""Identifies the logged-in user
appVersionString"1.0.0"Your app’s version string
accentColorColor#6C63FFFallback gradient color
visibilityConfigEmbedButtonVisibilityConfigshow everywhereScreen allow/exclude list and groups
navigationControllerUINavigationController?nilEnables automatic UIKit screen tracking

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)

Post EmbedViewDidAppear from .onAppear on each screen. The SDK listens for this notification and updates the current screen name.
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:
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

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

Fire the notification manually when your view becomes visible. You can also post EmbedViewDidDisappear on exit.
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

Pass the UINavigationController to .embedProvider() for automatic tracking:
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

By default the button shows on every screen. Use EmbedButtonVisibilityConfig to control this.

Show only on specific screens

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)

Use a group with .continuous continuity so the button doesn’t flash between screens:
visibilityConfig: EmbedButtonVisibilityConfig(
    groups: [
        EmbedButtonGroupConfig(
            id:          "checkout-flow",
            screens:     ["Cart", "Payment", "Confirmation"],
            continuity:  .continuous,
            delayMs:     0.5,
            delayPolicy: .oncePerGroupEntry
        )
    ]
)

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

EventKeys reference

KeyValueAuto-firedHost-callableNotes
.userDatauser_dataNoYesSend after login; must include app_user_id
.screenStatescreen_stateYesEmbedProviderYesFired on every screen enter and exit
.analyticsDataanalytics_dataYes — SDK UIYesSDK fires built-in events; host may also fire custom events
.customEventcustom_eventNoYesFree-form host-app events

Sending events

// 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

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

EventPayloadUse case
.agentConnectedtimestamp, metadata.callDuration: 0, server_urlPause background audio, start a timer
.agentDisconnectedtimestamp, metadata.callDuration (seconds)Resume audio, log call length
.popupMessageVisibleTrack tooltip impressions

Analytics helpers

These helpers fire analyticsData events with standardized payloads.
// 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

event_nameTrigger
agent_tap_to_openUser taps the collapsed FAB
agent_tap_to_closeUser taps avatar to close the card
agent_visibleEmbedProvider show-delay completes
popup_message_visibleInactivity nudge fires the tooltip
gen_tool_triggeredFirst data-channel message received
agent_conversation_startedLiveKit connect() succeeds
microphone_permission_allowOn every call start (status: already_granted or newly_granted)

Cleanup on logout

Call this when the user logs out or switches accounts to prevent stale data from leaking into the next session.
func onUserLogout() {
    EmbedSDK.shared.clearStorageCache()
}

Configuration reference

EmbedButtonVisibilityConfig

FieldTypeDefaultDescription
allowedScreens[String][] (all screens)Button shows only on these screens
excludedScreens[String][]Button always hidden on these screens
showDelayTimeInterval s0Delay before button fades in on allowed screens
groups[EmbedButtonGroupConfig][]Per-group override rules
defaultInsetEmbedButtonInset?SDK defaultDefault edge snap position

EmbedButtonGroupConfig

FieldTypeDefaultDescription
idStringrequiredUnique group identifier
screens[String]requiredScreens that belong to this group
continuityEmbedButtonContinuity.perScreen.continuous keeps the button mounted across group screens without flash
insetEmbedButtonInset?nilPer-group snap position override
delayMsTimeInterval s0Show delay for this group
delayPolicyEmbedButtonDelayPolicy.perScreenControls when the delay resets

EmbedButtonDelayPolicy

ValueBehaviour
.perScreenDelay applies every time the screen becomes active
.oncePerGroupEntryDelay only on the first entry into a group
.oncePerAppSessionDelay only on the very first view of the session

EmbedButton direct props

ParameterTypeDefaultDescription
appUserIdString""User identifier passed to the voice agent
accentColorColor#6C63FFFallback gradient
isVisibleBooltrueHides the button without unmounting it
insetEmbedButtonInsetright=24, bottom=80Snap position from screen edges

Troubleshooting

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

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: <error message>
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:
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.
// 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

Cause: .embedProvider() is applied inside a tab instead of outside the TabView.
// ❌ 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)

Cause: EmbedViewDidAppear notification not posted from .onAppear.
// ✅ 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

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

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

Always call clearStorageCache() on logout before the next user logs in:
func onUserLogout() {
    EmbedSDK.shared.clearStorageCache()
}

Pre-ship checklist

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

  • 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

  • visibilityConfig configured if the button should not show on all screens
  • .embedProvider() placed outside TabView (if applicable)

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