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
- Requirements
- How it works
- Step 1 — Add the package
- Step 2 — Add microphone permission
- Step 3 — Initialize the SDK
- Step 4 — Identify the user
- Step 5 — Add the floating button
- Routing scenarios
- Button visibility control
- Events
- Analytics helpers
- Cleanup on logout
- Configuration reference
- Troubleshooting
- Pre-ship checklist
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
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
# 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:
- Calls
GET /embedded-agent/initialize with your API key
- Parses and stores your widget configuration (colors, agent name, avatar)
- Pre-warms the Lottie animation cache
- Installs
ClickEventTracker for automatic rage-click detection
- 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.
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
| 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
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.
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"]
)
)
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
| 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
// 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
| 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
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_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
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
| 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 |
| 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 |
| 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 |
| 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
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.
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
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:
USER_DATA was sent with a valid app_user_id before other events
initialize() completed successfully (isInitialized == true)
- Device has internet connectivity
- 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
Screen tracking
Visibility
Production readiness
Support