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-nativeSelect 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.xcworkspaceStep 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/initializewith your API key - Parses and stores your widget configuration (colors, agent name, avatar)
- Pre-warms the Lottie animation cache
- Installs
ClickEventTrackerfor automatic rage-click detection - Sets
EmbedSDK.shared.isInitialized = trueon 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
| 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.
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 → HIDDENEvents
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
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
| 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
| 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
| 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.
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 trueCause 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 mismatchButton 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 tabsScreen 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_DATAwas sent with a validapp_user_idbefore other eventsinitialize()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
- Package added via Xcode SPM:
https://github.com/RevRag-ai/embed-native -
NSMicrophoneUsageDescriptionadded toInfo.plist -
await EmbedSDK.shared.initialize(apiKey:)called inApp.init()orAppDelegate -
USER_DATAevent sent withapp_user_idimmediately after login -
.embedProvider()applied to the root view (outsideTabViewif tabs are used) -
clearStorageCache()called on logout
Screen tracking
-
EmbedViewDidAppearnotification posted from every screen's.onAppear(SwiftUI NavigationStack) - Or
navigationControllerpassed to.embedProvider()for automatic UIKit tracking - Screen names in notifications match
visibilityConfigexactly (case-sensitive)
Visibility
-
visibilityConfigconfigured if the button should not show on all screens -
.embedProvider()placed outsideTabView(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
- Issues: GitHub Issues
- Docs: Revrag Documentation
- Dashboard: app.revrag.ai