Skip to main content
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…, enter https://github.com/traffical/ios-sdk, and add the Traffical library to your app target. Or in Package.swift:
The package supports iOS 14+, macOS 11+, tvOS 14+, and watchOS 7+, and has no third-party dependencies.

Quick start

The client is safe to call before 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:
Each getter takes an optional 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:
Events are batched in memory and flushed in the background (and on app backgrounding).

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:
Bucketing changes at login. The pre-login (UUID) bucket and the post-login (user_789) bucket are computed from different inputs, so a user may cross between variants at login. This is inherent to having a single unit of randomization per project. If you need assignments stable across login, run the experiment on logged-in users only, or call identify() before the SDK ever resolves the parameter.

Device-info provider

Attach a DeviceInfoProvider 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:
The provider’s fields are merged into the context on every resolution, so a policy condition like 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 and decide 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.