Traffical is the official Swift SDK for native iOS, macOS, tvOS, and watchOS apps. It resolves parameters locally from a config bundle in sub-millisecond time, with mobile-native defaults: a persistent on-disk cache, a Keychain-backed stable ID that survives reinstall, foreground refresh, offline graceful degradation, and an embedded localConfig bundle for cold starts.
It shares the language-agnostic Traffical SDK spec with the TypeScript, PHP, and Python SDKs — the same SHA-256 v2 bucketing, the same layered resolution engine, the same contextual-bandit scoring — so a given unit buckets identically on every platform.
Installation
Swift Package Manager
In Xcode: File → Add Packages…, enterhttps://github.com/traffical/ios-sdk, and add the Traffical library to your app target.
Or in Package.swift:
Quick start
initialize() returns — it seeds synchronously from localConfig, then the disk cache, then your inline defaults, so the first typed getter always has something to work with. initialize() upgrades that to fresh network state and starts the background refresh.
Lifecycle
The lifecycle verbs follow the cross-language contract:waitForReady(timeoutMs:) resolves once the first usable config is loaded, or the fail-open window elapses — it never hangs on an unavailable or malformed bundle. close() is the single teardown verb: it cancels the background refresh and awaits a final event flush before returning.
Typed getters
The typed getters resolve a single parameter against an inline default and auto-track an exposure the first time an assignment is seen in the session. There is one per value type:context: argument for targeting; when omitted, the SDK uses the stable ID (plus any device-info fields) as context.
Decisions and getParams
When you need the full decision — or want to delay the exposure until the variant is actually shown — use decide(context:defaults:). It returns a TrafficalDecisionResult with a decisionId, the resolved assignments, and per-layer metadata. Both decide and getParams take context first, defaults second:
getParams(context:defaults:) returns just the assignment map (no metadata) and does not auto-track exposure. trackExposure(_:) emits at most one exposure event per call (spec S4), carrying only newly-exposed, non-attribution-only layers, session-deduped by default.
Tracking events
track(_:properties:options:) — the event name first, an optional properties payload, and a TrackOptions bag (decisionId, unitKey, value, values, eventTimestamp). unitKey defaults to the current stable ID, so you rarely pass it:
Options
TrafficalClientOptions uses the canonical cross-language option names. Duration options are milliseconds with a *Ms suffix.
Baked bundle (localConfig)
Pass a localConfig bundle to resolve instantly on cold start — before any network call, and even fully offline. The client seeds from it synchronously in init, so the first string(...) / decide(...) already resolves real assignments; a successful refresh then upgrades to fresh config. Bake the bundle into your app at build time (for example, generated by the CLI) so a fresh install has working config on first launch.
Stable ID and identify
On first launch the SDK generates an anonymous UUID and persists it in the Keychain, so it survives app reinstall (matching what analytics SDKs do). This value fills the project’s unit-key slot on every resolution, so bucketing and exposures work before the user logs in.
Read it with getStableId(). When the user logs in, call identify(_:) to swap in the real unit key:
Device-info provider
Attach aDeviceInfoProvider to enrich the evaluation context with device fields — app version, OS, locale, screen size — so you can target experiments by them. The bundled DefaultDeviceInfoProvider reads app version/build, locale, timezone, OS name/version, and (on UIKit platforms) screen dimensions and device model:
appBuildNumber gte 500 or locale == "en_US" can target them. Implement the DeviceInfoProvider protocol yourself to add or override fields.
Server vs bundle mode
In the default.bundle mode the SDK fetches the config bundle and resolves every parameter locally — sub-millisecond, no per-decision network call. In .server mode each resolution delegates to the edge worker via POST /v1/resolve (cached per context), for when you want zero client-side evaluation logic:
Fail-open behavior
Every public call is wrapped in an error boundary and fails open to your inline defaults. If the bundle isn’t loaded yet — or Traffical is unreachable entirely — typed getters anddecide return the defaults you passed, and waitForReady resolves rather than hanging. Traffical can only raise the ceiling, never lower the floor.
Cross-language conformance
The Swift engine (TrafficalCore) is validated against the spec’s deterministic conformance vectors — the same SHA-256 v2 (UTF-8 byte) assignment hashing, layered resolution, and contextual-bandit scoring as every other Traffical SDK — plus events-payload validation against events.schema.json, so a given unit buckets identically on every platform.
Next steps
A/B testing
Run a static experiment.
Canonical experiments
Patterns for backend, batch, and cross-surface tests.
CLI
Bake a config bundle into your app.
API reference
The endpoints the SDK calls.