Skip to Content
DevelopersSDKsiOS SDKQuickstart

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 are ActiveReachPush, ActiveReachInApp, ActiveReachNotificationService, ActiveReachLocation), but the Swift class you call is still Aegis (with AegisConfig, AegisPushTracker, etc.). Swift Package Manager + the podspec map the customer-facing module names to the internal source tree at Sources/AegisCore/, Sources/AegisPush/, and so on — “Aegis” is the internal name; “Active Reach” is the customer-facing identifier on CocoaPods + SPM.

Installation

In Xcode: File → Add Packages → enter the repository URL:

https://github.com/active-reach/ios-sdk

Select 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:

  1. A coalesced aegis.client.name_governor_dropped meta event rides the next batch flush — ops dashboards see the pattern
  2. In DEBUG builds, a single print() 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).

The NSE honors the host app’s per-channel marketing consent before posting any delivery telemetry. The flow:

  1. The host app’s ConsentManager persists category preferences to the shared App Group UserDefaults (key ai.aegis.consent.preferences).
  2. When a push arrives, the NSE reads the marketing category from that shared record.
  3. If marketing is denied (or no record exists yet — the host hasn’t shown its CMP), the NSE silently skips the /v1/push/engagement POST. The OS still displays the notification.
  4. 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:

LifecycleSourceWhat it posts
DeliveredThe bundled NotificationService extension (UNNotificationServiceExtension subclass)event_type: "push.delivered"
ClickedYour host app’s UNUserNotificationCenterDelegate.didReceiveevent_type: "push.clicked" with metadata.action_id
DismissedYour host app’s UNUserNotificationCenterDelegate.didReceive with actionIdentifier == UNNotificationDismissActionIdentifierevent_type: "push.dismissed"
SubscribedAegis.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