iOS SDK
The Active Reach iOS SDK tracks user behavior, identifies users, and manages push notification tokens on iOS 13+.
Current version: ActiveReachSDK 1.6.0 (Swift Package + CocoaPods).
Brand canon. The Swift module + podspec name is
ActiveReachSDK(and the optional companions areActiveReachPush,ActiveReachInApp,ActiveReachNotificationService,ActiveReachLocation), but the Swift class you call is stillAegis(withAegisConfig,AegisPushTracker, etc.). Swift Package Manager + the podspec map the customer-facing module names to the internal source tree atSources/AegisCore/,Sources/AegisPush/, and so on — “Aegis” is the internal name; “Active Reach” is the customer-facing identifier on CocoaPods + SPM.
Installation
Swift Package Manager (recommended)
In Xcode: File → Add Packages → enter the repository URL:
https://github.com/active-reach/ios-sdkSelect 1.6.0 (or Up to Next Major) and add the ActiveReachSDK product to your target. Add ActiveReachPush, ActiveReachInApp, ActiveReachNotificationService, and/or ActiveReachLocation as optional companion products if you need those features.
CocoaPods
pod 'ActiveReachSDK', '~> 1.6'
# Optional companions — add only what you need:
# pod 'ActiveReachPush', '~> 1.6'
# pod 'ActiveReachInApp', '~> 1.6'
# pod 'ActiveReachNotificationService', '~> 1.6'
# pod 'ActiveReachLocation', '~> 1.6'Then run pod install.
Initialization
Initialize the SDK in your AppDelegate:
import ActiveReachSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
Aegis.shared.initialize(
writeKey: "YOUR_WRITE_KEY",
config: AegisConfig(workspaceId: "YOUR_WORKSPACE_ID")
)
return true
}
}Core tracking
// Track a custom event
Aegis.shared.track("order_completed", properties: [
"order_id": "ORD-123",
"amount": 4999,
"currency": "INR"
])
// Identify a user
Aegis.shared.identify("user_456", traits: [
"name": "Priya Sharma",
"email": "[email protected]",
"plan": "pro"
])
// Track a screen view
Aegis.shared.page("Product Detail", properties: [
"product_id": "SKU-A"
])
// Group a user into a company
Aegis.shared.group("company_789", traits: [
"name": "Acme Corp"
])
// Merge anonymous + identified sessions
Aegis.shared.alias("user_456")
// Clear identity on logout
Aegis.shared.reset()
// Flush before app termination
Aegis.shared.flush()Event governance
The iOS SDK ships with client-side event-name cap enforcement — byte-identical hashing with the web SDK, Python control-plane, and Android SDK. When you bootstrap the SDK, the server returns a compact Bloom filter of your registered event names + a counter of remaining headroom. The SDK uses this to drop novel names locally once you hit your plan cap.
Integration
import ActiveReachSDK
// 1. Bootstrap against control-plane to get the hint.
// (Typically done by your app's onboarding code — reuses the same
// /v1/sdk/bootstrap response that gives you VAPID keys + property IDs.)
let bootstrap = try await bootstrap(
apiHost: "https://api.active-reach.ai",
writeKey: YOUR_WRITE_KEY
)
// 2. Initialize the SDK.
Aegis.shared.initialize(writeKey: YOUR_WRITE_KEY, config: AegisConfig(
apiHost: "https://api.active-reach.ai"
))
// 3. Feed the hint to the governor.
Aegis.shared.ingestGovernanceHint(bootstrap.eventGovernance)Pass nil to disable governance (fail-open — gateway remains authoritative):
Aegis.shared.ingestGovernanceHint(nil)What the SDK does
Identical semantics to the web SDK — bloom-check for known names, per-instance memo of novel names so repeating a name doesn’t double-charge, drop-and-coalesce for novel names past the cap, fail-open during the server’s 7-day grace window.
Drops surface in two ways:
- A coalesced
aegis.client.name_governor_droppedmeta event rides the next batch flush — ops dashboards see the pattern - In
DEBUGbuilds, a singleprint()on the first dropped name per session — developers see the issue in Xcode immediately
The EventGovernanceHint type
public struct EventGovernanceHint: Codable {
public let bloomAlgo: String // always "mmh3_x86_32_km"
public let seedA: UInt32 // always 0
public let seedB: UInt32 // always 1
public let k: Int // typically 7
public let m: Int // power-of-2 bitarray size
public let bloomB64: String // base64 of the bitarray
public let remainingNewNames: Int? // nil = Enterprise / unlimited
public let graceActive: Bool // server in 7-day grace? SDK fails open when true
public let ttlSeconds: Int // re-bootstrap if older than this
}Decodes directly from the /v1/sdk/bootstrap JSON response — CodingKeys are set up for snake_case JSON.
Thread-safety
NameGovernor uses an internal serial DispatchQueue — all of ingestHint, shouldSend, and drainDropReport are safe to call from any thread concurrently.
Push notifications
Register the device token after obtaining push permission:
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
Aegis.shared.push.register(deviceToken: deviceToken)
}Track delivery and click events:
Aegis.shared.push.trackDelivery(messageId: "msg_123")
Aegis.shared.push.trackClick(messageId: "msg_123")Requires APNs setup — see Push setup guide.
Notification Service Extension (rich push + delivery telemetry)
For rich push attachments (images, video) plus accurate push.delivered telemetry, add an Active Reach Notification Service Extension to your Xcode project. The NSE runs in a separate process from the host app — it downloads the media attachment, fires the delivery callback, and lets the OS render the notification.
import AegisNotificationService
class NotificationService: AegisNotificationService { }The extension reads its config from an App Group UserDefaults suite that the host app writes to via AegisPushTracker.configure(appGroupSuiteName:). Set the suite name in both the host app and the extension’s Info.plist (key: AegisNotificationServiceAppGroup).
Consent gating in the extension (Phase 4.5)
The NSE honors the host app’s per-channel marketing consent before posting any delivery telemetry. The flow:
- The host app’s
ConsentManagerpersists category preferences to the shared App GroupUserDefaults(keyai.aegis.consent.preferences). - When a push arrives, the NSE reads the
marketingcategory from that shared record. - If marketing is denied (or no record exists yet — the host hasn’t shown its CMP), the NSE silently skips the
/v1/push/engagementPOST. The OS still displays the notification. - Once the customer grants marketing consent, the next push delivery posts telemetry normally.
This is the iOS counterpart to the web SDK’s setOptIn mirror requirement — both surfaces gate delivery telemetry on the same consent state so segmenters, journey channel pickers, and the DPDP audit trail see consistent opt-in records. The OS-visible notification is independent: consent affects what we measure, not what the customer sees.
The cross-SDK contract is pinned by libs/mobile-sdk/tests/drift/phase45-cross-platform.json (ios_nse_consent_gating entry). Full setup walkthrough → iOS SDK Advanced → Rich notifications.
Push engagement reporting (SDK ≥1.4.0)
AegisPushTracker posts push lifecycle events to the canonical
ingestion endpoint. Two integration points cover the lifecycle:
| Lifecycle | Source | What it posts |
|---|---|---|
| Delivered | The bundled NotificationService extension (UNNotificationServiceExtension subclass) | event_type: "push.delivered" |
| Clicked | Your host app’s UNUserNotificationCenterDelegate.didReceive | event_type: "push.clicked" with metadata.action_id |
| Dismissed | Your host app’s UNUserNotificationCenterDelegate.didReceive with actionIdentifier == UNNotificationDismissActionIdentifier | event_type: "push.dismissed" |
| Subscribed | Aegis.shared.push.register(deviceToken:) | event_type: "push.subscribed" |
Add the Notification Service Extension target in Xcode and import
AegisNotificationServiceExtension — the bundled NotificationService
class handles the delivered POST inside the extension’s ~30-second
budget. Click and dismiss must originate from the main app process:
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
Aegis.shared.push.handleNotificationResponse(response)
completionHandler()
}
}Wire shape: POST /v1/push/engagement — see
Event ingestion.
In-app messaging
Aegis.shared.inApp.show(campaignId: "campaign_123")
Aegis.shared.inApp.dismiss()Advanced features like deep linking, rich push attachments, and app lifecycle hooks are documented in the full iOS SDK reference. Coming soon.
E-commerce helpers (Phase 1, SDK ≥1.1.0)
Aegis.shared.ecommerce is the canonical 19-method tracker — parity with the web SDK’s EcommerceTracker, so the cell-plane consumes events identically across web + iOS.
let product = EcommerceProduct(productId: "sku-42", name: "Cotton Tee", price: 1299.0)
Aegis.shared.ecommerce.productViewed(product)
Aegis.shared.ecommerce.addToCart(product)
Aegis.shared.ecommerce.orderCompleted(EcommerceOrder(
orderId: "ord-100",
value: 1299.0,
products: [product]
))
// Back-in-stock waitlist — arms the catalog.back_in_stock journey trigger.
Aegis.shared.ecommerce.productWaitlisted(EcommerceWaitlist(
product: product,
channels: [.whatsapp]
))Trait governance (Phase 1, SDK ≥1.1.0)
identify(_:traits:) and group(_:traits:) run traits through the same 5 ingestion guards as the cell-plane backend. Drops emit os_log warnings rate-limited to first 3 per (workspace, verdict).
Multi-region cell selection (Phase 1, SDK ≥1.1.0)
let config = AegisConfig(
cellEndpoints: [
CellEndpoint(region: "ap-south", endpoint: "https://ap-south.api.aegis.ai"),
CellEndpoint(region: "eu-central", endpoint: "https://eu-central.api.aegis.ai"),
],
preferredRegion: "ap-south",
autoRegionDetection: false,
workspaceId: "ws_abc123"
)
Aegis.shared.initialize(writeKey: "YOUR_WRITE_KEY", config: config)/v1/sdk/bootstrap handshake (Phase 1, SDK ≥1.1.0)
Aegis.shared.bootstrap { result in
switch result {
case .success(let res):
// res.propertyId, res.workspaceId, res.locationCodes
// NameGovernor is auto-armed with res.eventGovernance.
break
case .failure(let err):
// 401/403 → install can't prove origin ownership.
break
}
}What’s next
- Push primer (soft-ask) — lift opt-in with a pre-permission card
- Geofencing & location — outlet enter/exit events (opt-in)
- Push notification setup
- In-app messaging setup
- Example app — a runnable SwiftUI sample wiring tracking, identify, and lifecycle