Skip to Content
StreamsiOS SDK

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' # optional

Initialise

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.

OptionTypeDefaultDescription
apiHostString—Ingest base URL (required)
namespaceString"zeotap"Storage-key prefix ({namespace}_anonymous_id, …), shared with the browser SDK
flushAtInt10Send after this many queued events
flushIntervalInt5000Send every N milliseconds
debugBoolfalseVerbose os.Logger output (subsystem com.zeotap.sdk)
nativeAppIdString—Snowflake Native App install id
consentConsentOptionsattach-onlyOpt-in gating, defaults, TCF — see below
integrationIntegrationMeta?—Wrapping integration → context.library.integration (the React Native bridge sets this)
trackApplicationLifecycleEventsBoolfalseEmit Application Installed / Updated / Opened / Backgrounded
collectAdvertisingId + advertisingIdProviderBool + providerfalseAttach the IDFA — provide ATTAdvertisingIdProvider() from ZeotapSDKAdSupport; the SDK never prompts for ATT
maxQueueSizeInt500Durable queue capacity; the oldest event is dropped beyond it
maxEventAgeDaysInt7Queued events older than this are pruned at launch
flushOnBackgroundBooltrueSend queued events when the app moves to the background
identityStorage.userDefaults / .keychain.userDefaultsKeychain survives reinstalls — a deliberate identity-semantics change
callbackQueueDispatchQueue.mainWhere completions and SdkListener calls run
logger / storage—os.Logger / UserDefaultsInject 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 cleared

There 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.

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, gdprApplies

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

BlockFields
context.appname, version, build, namespace
context.deviceid (identifierForVendor), manufacturer: "Apple", model, type: "ios", advertisingId / adTrackingEnabled (only with collectAdvertisingId)
context.osname, version
context.networkwifi, cellular (no carrier — CoreTelephony is deprecated)
context.screenwidth, height, density
context.locale, context.timezone, context.userAgentBCP-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

  • configure throws invalidApiHost — apiHost must be an http(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 reports onDeliveryFailure(status: 401, …).
  • Events accepted but nothing downstream — a write key in pending_schema status returns 200 and publishes nothing until its schema is reviewed in Streams > Event Sources.
Last updated on