iOS SDK
The Zeotap iOS SDK (ZeotapSDK) captures events from your iOS app and sends them to the ingest API. It is the iOS counterpart of the Browser SDK: the same event model, the same consent model, the same on-device identity hashing and the same wire format — proven identical by a shared conformance suite generated from the browser SDK — plus what a mobile app needs that a web page does not: a durable offline queue, flush on background, device/app context and app-lifecycle events.
Swift, iOS 14+, no third-party dependencies. Three products: ZeotapSDK (the SDK), ZeotapSDKAdSupport (optional IDFA support — the only piece that links tracking frameworks) and ZeotapSDKObjC (Objective-C wrappers, also used by the React Native bridge).
Install
Swift Package Manager — add the package and the ZeotapSDK product to your target:
// Package.swift
.package(url: "https://github.com/zeotap/zt-ios-sdk.git", from: "0.1.0")or in Xcode: File › Add Package Dependencies… with the repository URL, then select ZeotapSDK (and ZeotapSDKAdSupport only if you collect the advertising identifier).
CocoaPods:
pod 'ZeotapSDK', '~> 0.1'
pod 'ZeotapSDK/AdSupport' # optionalInitialise
Call Zeotap.configure once, as early as possible (application(_:didFinishLaunchingWithOptions:) or your App init). Later calls return the existing instance.
import ZeotapSDK
try Zeotap.configure(writeKey: "YOUR_WRITE_KEY") { c in
c.apiHost = "https://events.zeotap.com"
c.consent.require = true
c.trackApplicationLifecycleEvents = true
}Replace YOUR_WRITE_KEY with the key from Streams > Event Sources in your workspace. apiHost is required (there is no “same origin” on a device); point it at the ingest host or at a first-party proxy on your own domain. configure throws on an empty write key or an invalid apiHost.
Configuration
Option names and units match the browser SDK’s LoadOptions, so a React Native app passes one options object to both platforms.
| Option | Type | Default | Description |
|---|---|---|---|
apiHost | String | — | Ingest base URL (required) |
namespace | String | "zeotap" | Storage-key prefix ({namespace}_anonymous_id, …), shared with the browser SDK |
flushAt | Int | 10 | Send after this many queued events |
flushInterval | Int | 5000 | Send every N milliseconds |
debug | Bool | false | Verbose os.Logger output (subsystem com.zeotap.sdk) |
nativeAppId | String | — | Snowflake Native App install id |
consent | ConsentOptions | attach-only | Opt-in gating, defaults, TCF — see below |
integration | IntegrationMeta? | — | Wrapping integration → context.library.integration (the React Native bridge sets this) |
trackApplicationLifecycleEvents | Bool | false | Emit Application Installed / Updated / Opened / Backgrounded |
collectAdvertisingId + advertisingIdProvider | Bool + provider | false | Attach the IDFA — provide ATTAdvertisingIdProvider() from ZeotapSDKAdSupport; the SDK never prompts for ATT |
maxQueueSize | Int | 500 | Durable queue capacity; the oldest event is dropped beyond it |
maxEventAgeDays | Int | 7 | Queued events older than this are pruned at launch |
flushOnBackground | Bool | true | Send queued events when the app moves to the background |
identityStorage | .userDefaults / .keychain | .userDefaults | Keychain survives reinstalls — a deliberate identity-semantics change |
callbackQueue | DispatchQueue | .main | Where completions and SdkListener calls run |
logger / storage | — | os.Logger / UserDefaults | Inject your own log sink or (e.g. encrypted) key-value store |
Events
Every method is safe to call from any thread; work is posted to the SDK’s serial queue in call order. Optional completions receive nil on success or a ZeotapError describing why the call was dropped.
Zeotap.track("Order Completed", properties: ["order_id": "o-1", "total": 99.5, "currency": "USD"])
Zeotap.screen("Product", category: "Shop", properties: ["sku": "A-1"])
Zeotap.identify(userId: "user-42", traits: ["plan": "pro"])
Zeotap.group("acme", traits: ["plan": "enterprise"])
Zeotap.alias("user-42") // previousId defaults to the anonymous id
Zeotap.flush()
Zeotap.reset() // logout: new anonymous id, identities/consent/queue clearedThere is no page() on iOS — screen() is the mobile primitive. Property dictionaries accept strings, numbers, booleans, NSNull, Date, URL, nested dictionaries and arrays; an unsupported value fails that call with ZeotapError.invalidProperties instead of crashing. Whole doubles serialise as integers (1.0 → 1), as in JavaScript.
User identities
Zeotap.setUserIdentities([
"email": "Shopper@Example.com", // → sha256/sha1 × lowercase/uppercase
"cellno": "91 9999999999", // → cellno {sha256, sha1} + cellno_e164 (cc+number)
"fpuid": "crm-123", // first-party ids pass through unhashed
])
Zeotap.setUserIdentities(["email_sha256_lowercase": "…"], hashed: true) // pre-hashed input
Zeotap.unsetUserIdentities(["email"])Raw email / phone / login values are normalised and hashed on the device and never leave it. The hashed map persists and rides on every later event as context.identities, exactly like the browser SDK. Setting identities is gated by the identify consent signal; unsetting never is.
Consent
ConsentOptions mirrors the browser SDK — see Consent management. With the defaults, events flow immediately and consent is merely attached to payloads. Gating activates with require = true (opt-in) or default = .denied (opt-out-by-default).
c.consent.require = true // buffer up to maxBufferedEvents until consent resolves
c.consent.cmpTimeoutMs = 10_000 // then fall back to `default`
c.consent.cmp = .auto // read a CMP's IAB TCF-for-Apps defaults (default when require = true)
c.consent.purposesForTracking = [1, 3, 4]
Zeotap.setConsent(.parsed(ParsedConsent(analytics: true, advertising: false)))
Zeotap.setConsent(.tcf(tcfString: "CQ…"))
Zeotap.setConsent(.usPrivacy("1YNN"))
let state = Zeotap.getConsentState() // resolved, signals, source, gdprAppliesTCF v2 on iOS means the IAB TCF for Apps keys your CMP SDK writes to UserDefaults.standard (IABTCF_CmpSdkID, IABTCF_gdprApplies, IABTCF_TCString, IABTCF_PurposeConsents, IABTCF_VendorConsents). With cmp = .auto the SDK detects a CMP within cmpTimeoutMs, translates purposes and the Zeotap vendor bit (GVL 1469, legacy 301) with the same rules as the browser SDK, attaches the TC string to every event, and re-resolves when the CMP updates the defaults. Resolved decisions persist across launches (persist, expiryDays).
Context
Every event carries, in addition to the browser fields:
| Block | Fields |
|---|---|
context.app | name, version, build, namespace |
context.device | id (identifierForVendor), manufacturer: "Apple", model, type: "ios", advertisingId / adTrackingEnabled (only with collectAdvertisingId) |
context.os | name, version |
context.network | wifi, cellular (no carrier — CoreTelephony is deprecated) |
context.screen | width, height, density |
context.locale, context.timezone, context.userAgent | BCP-47 tag, IANA zone id, ZeotapSDK/<version> (<model>; <os> <version>) |
Privacy defaults: no device name, no carrier, IDFA only with the AdSupport product and an authorised ATT status.
Lifecycle events
With trackApplicationLifecycleEvents = true the SDK tracks Application Installed {version, build}, Application Updated {version, build, previous_version, previous_build}, Application Opened {from_background, version, build} and Application Backgrounded. They go through the consent gate like any other event.
Delivery & offline
Events are written to a SQLite queue (excluded from backups, completeUntilFirstUserAuthentication protection) before the call returns. A batch is reserved while in flight and acknowledged on a 2xx; a batch still reserved at the next launch is re-sent (at-least-once — downstream dedupes on messageId). Retry policy is the browser SDK’s: 4 attempts with 500/1000/2000 ms back-off on 5xx, 429 and network errors; any other 4xx drops the batch. After exhausted retries the batch stays queued and the SDK backs off (30 s → 5 min) until the network returns or the app comes to the foreground. Backgrounding flushes inside a beginBackgroundTask window.
Ingest answers 200 even on partial failure. Register an SdkListener to see BatchResult.success / errors, delivery failures and dropped events.
App Store privacy
The SDK ships privacy manifests: ZeotapSDK declares NSPrivacyTracking = false and the UserDefaults access reason CA92.1; ZeotapSDKAdSupport declares NSPrivacyTracking = true. For your app’s nutrition label the SDK collects Device ID (identifierForVendor; the IDFA only with AdSupport), User ID (when you call identify) and Product Interaction (events), linked to the user, not used for tracking unless you enable the advertising id — in which case your app must also declare its apiHost under NSPrivacyTrackingDomains.
Troubleshooting
configurethrowsinvalidApiHost—apiHostmust be anhttp(s)://URL; the browser default of an empty host has no meaning on iOS.- 401
write key not found— the SDK drops the batch and reportsonDeliveryFailure(status: 401, …). - Events accepted but nothing downstream — a write key in
pending_schemastatus returns 200 and publishes nothing until its schema is reviewed in Streams > Event Sources.