> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://contentful.com/developers/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://contentful.com/developers/docs/_mcp/server.

# iOS SDK runtime and interaction mechanics

> Understand how the Optimization iOS SDK runs shared optimization behavior in a native app, how SwiftUI and UIKit integrations share the same client, and how consent, state, entry resolution, tracking, preview overrides, and offline delivery work.

## Overview

Use this concept document to understand how the Optimization iOS SDK runs shared optimization
behavior in a native app, how SwiftUI and UIKit integrations share the same client, and how consent,
state, entry resolution, tracking, preview overrides, and offline delivery work.

This document applies to native apps on iOS 15 or later. SwiftUI and UIKit
sections name runtime-specific APIs where behavior differs.

For step-by-step setup, see
[Integrate the Optimization iOS SDK in a SwiftUI app](/personalization/optimization-sdk/integrate-the-optimization-ios-sdk-in-a-swiftui-app/)
and
[Integrate the Optimization iOS SDK in a UIKit app](/personalization/optimization-sdk/integrate-the-optimization-ios-sdk-in-a-uikit-app/).
For the full Contentful entry contract, see
[Entry optimization and variant resolution](/personalization/optimization-sdk/entry-personalization-and-variant-resolution/).

## Runtime boundary

The iOS SDK is a native Swift Package named `ContentfulOptimization`. Swift owns native app concerns
such as persistence, networking, lifecycle handling, SwiftUI helpers, UIKit preview-panel
presentation, and app-facing public APIs.

Shared optimization behavior runs inside a local JavaScriptCore context. That bridge lets the iOS
SDK use the same optimization, profile, consent, and event-delivery behavior as the JavaScript SDKs
while exposing a Swift API to the application.

Applications do not call the JavaScript layer directly. The public boundary is Swift:

* `OptimizationClient` is the main facade for initialization, state, optimization, tracking, and
  preview controls.
* `OptimizationRoot`, `OptimizedEntry`, `OptimizationScrollView`, and `.trackScreen(name:)` provide
  SwiftUI integration helpers.
* `PreviewPanelViewController` provides the UIKit preview-panel host.

This split also defines what the SDK does not own. The application still fetches Contentful entries,
manages consent UX, controls routing, decides identity policy, and renders the final UI.

## Prerequisites and runtime constraints

Decide these policies before initialization because they shape the client state the bridge receives
at startup and the events it can emit before runtime consent changes:

| Constraint           | iOS behavior                                                                                                                                                                                                                                                                                                                                                                                                           |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Configuration        | `spaceId` is required. `environment` defaults to `"master"`. `locale` configures the SDK Experience API and event locale; it does not choose the Contentful CDA locale for app-owned entry fetches.                                                                                                                                                                                                                    |
| Consent              | `state.consent` starts as unset unless `StorageDefaults.consent` or persisted SDK consent provides a value. Until event consent is `true`, iOS/native allow-list behavior lets only `identify` and `screen` emit by default.                                                                                                                                                                                           |
| Persistence consent  | Boolean `client.consent(true)` or `client.consent(false)` updates event consent and durable profile-continuity persistence consent together. Use split consent when event consent and durable profile continuity have separate policy decisions.                                                                                                                                                                       |
| Allowed event types  | `OptimizationConfig.allowedEventTypes` replaces the native default pre-consent allow-list. Pass `allowedEventTypes: []` for strict opt-in before any Optimization event, or pass a narrow custom list when legal and privacy review permits specific pre-consent events.                                                                                                                                               |
| Storage availability | iOS stores consent and, when persistence consent is `true`, profile-continuity values in `UserDefaults`. If storage has no usable value or is cleared, the SDK starts from configured defaults and does not restore profile-continuity state from a previous process.                                                                                                                                                  |
| Preview mode         | The preview panel is an app opt-in surface. Mount it only in debug or internal flows. Opening the panel sets `client.isPreviewPanelOpen`; SwiftUI `OptimizedEntry` treats that state as a live-update override, while UIKit apps must subscribe and redraw to reflect preview changes.                                                                                                                                 |
| Offline behavior     | Event queues are in memory. Events queued while offline flush when connectivity returns. On iOS/UIKit, app backgrounding triggers an online best-effort flush of queued events; it does not make an offline queue durable or flush while the device remains offline. `QueuePolicy` can tune caps, retry, backoff, circuit behavior, and callbacks, but the SDK does not provide a durable outbox across process death. |
| Configured defaults  | `StorageDefaults` are startup defaults and take precedence over persisted values. If the application persists user choices, leave consent and persistence defaults unset. Restore SDK-stored consent naturally, or call `client.consent(...)` from the resolved app policy instead of seeding every launch.                                                                                                            |

## Lifecycle and main actor

`OptimizationClient` has two phases:

| Phase         | Behavior                                                                                                                                                                                                                                 |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Uninitialized | The client exists, but the bridge is not loaded. Async event APIs and `setLocale(_:)` throw `OptimizationError.notInitialized`; many sync read, resolve, consent, reset, and online-state APIs return `nil`, baseline content, or no-op. |
| Initialized   | The bridge is loaded, startup defaults and eligible persisted state have been resolved, SDK state is available, network observers are active, and iOS/UIKit app lifecycle observers are active when UIKit is available.                  |

SwiftUI apps usually let `OptimizationRoot` call `initialize(config:)`. UIKit apps usually call
`initialize(config:)` from scene or app startup before passing the client into view controllers.

`OptimizationClient` is `@MainActor`. Call it from main-thread contexts such as SwiftUI view tasks,
SwiftUI event handlers, view-controller lifecycle methods, or `Task { @MainActor in ... }` blocks.
The compiler can flag background calls as concurrency errors.

Typical apps keep one `OptimizationClient` alive for the app or scene lifetime. Use `destroy()` for
test teardown or deliberate SDK reset flows.

## Configuration and locale handoff

Every iOS integration builds an `OptimizationConfig`:

```swift
OptimizationConfig(
    spaceId: "your-space-id",
    environment: "master",
    locale: "en-US",
    logLevel: .debug
)
```

Only `spaceId` is required. `environment` defaults to `"master"`. Base URL overrides belong only in
integrations that need non-default Experience API or Insights API endpoints.

Use top-level `locale` for the SDK Experience/event locale. When the application renders localized
Contentful entries, query the Contentful Delivery API by content type and slug with one app-owned
locale, then pass that fetched entry to `OptimizedEntry` or `resolveOptimizedEntry(...)`. The native
iOS SDK does not provide managed entry fetching.

For the full locale model, see
[Locale handling in the Optimization SDK Suite](/personalization/optimization-sdk/locale-handling-in-the-optimization-sdk-suite/).

## State and persistence

`OptimizationClient` is an `ObservableObject`. It publishes runtime state that SwiftUI and UIKit
code can observe:

| Surface                  | Description                                                                                                        |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `state`                  | Snapshot of profile, consent, optimization readiness, and pending changes.                                         |
| `isInitialized`          | `true` after initialization completes.                                                                             |
| `selectedOptimizations`  | The selected optimization variants for the visitor.                                                                |
| `state.canOptimize`      | Whether selected optimization data is available for entry resolution.                                              |
| `optimizationPossible`   | Whether the current consent and allow-list configuration can produce optimization data.                            |
| `experienceRequestState` | Outcome of the most recent Experience API request.                                                                 |
| `locale`                 | Current SDK locale for Experience API requests and event context.                                                  |
| `isPreviewPanelOpen`     | `true` while the in-app preview panel is visible.                                                                  |
| `previewState`           | Preview override state used by the in-app preview panel.                                                           |
| `eventStream`            | Public passthrough event stream for debug surfaces and tests. It does not replay prior events to late subscribers. |
| `blockedEventStream`     | Public passthrough stream for events blocked by consent or allow-list gating.                                      |

SwiftUI code reads these values through `@EnvironmentObject`. UIKit code can subscribe through
Combine publishers such as `client.$state` and `client.$selectedOptimizations`.

Use `state.canOptimize` when rendering depends on variant data being available. Use
`optimizationPossible` when the app needs to know whether current consent and allow-list settings
can produce optimization data at all. For the cross-SDK distinction, see
[Core state management](/personalization/optimization-sdk/core-state-management/#key-state-definitions).

Custom Flags use the same Core-backed model as the Web SDKs: `client.getFlag(_:)` returns the
current JSON value, and `client.flagPublisher(_:)` returns an `AnyPublisher<JSONValue?, Never>` that
updates on distinct value changes. Reading or subscribing to a flag can emit deduplicated flag-view
events when consent and profile state allow it, so treat flag delivery as an analytics exposure.

For the lower-level state model shared through the native bridge, see
[Core state management](/personalization/optimization-sdk/core-state-management/).

The SDK persists consent with `UserDefaults`. When persistence consent is `true`, it also persists
profile-continuity values such as profile, selected changes, selected optimizations, and the
anonymous ID. `StorageDefaults` are startup defaults, not one-time seeds. During initialization,
configured defaults take precedence over persisted SDK values, so a configured consent or
persistence default can replace a visitor's stored choice on each launch.

Use `StorageDefaults(consent: true)` only when the application's policy is truly default-on at
startup. If the application persists user choices in a CMP, account setting, or app preference,
leave `StorageDefaults.consent` and `StorageDefaults.persistenceConsent` unset. Resolve the policy
at startup and call `client.consent(...)` after the client is available instead of seeding a choice
every launch. If the application relies on SDK consent storage, also leave those defaults unset so
the persisted SDK consent can restore naturally.

When durable profile-continuity persistence is allowed, the client writes profile-continuity values
to `UserDefaults` before publishing the corresponding state snapshot and selected optimizations.
Application code and XCUITest flows can wait for SDK-derived state rather than adding storage-timing
delays before relaunching.

## Consent and event gates

Consent is a three-state value: `true`, `false`, or unset. Until consent is granted, the SDK blocks
event types that are not allow-listed. iOS uses the native default allow-list when
`OptimizationConfig.allowedEventTypes` is unset:

| Consent state | Event behavior                                              |
| ------------- | ----------------------------------------------------------- |
| Unset         | `identify` and `screen` can emit; other events are blocked. |
| `true`        | All SDK event types can emit.                               |
| `false`       | `identify` and `screen` can emit; other events are blocked. |

This is the default iOS/native behavior, not a universal SDK rule. `allowedEventTypes` replaces the
default allow-list. Use `allowedEventTypes: []` when no Optimization event can emit before explicit
consent, or pass a custom list when policy permits specific pre-consent events. Allow-listed
pre-consent events still mark `context.gdpr.isConsentGiven` as `false` until event consent is
explicitly `true`.

Call `client.consent(true)` when the visitor grants consent and `client.consent(false)` when the
visitor rejects it. Boolean consent controls both event emission and durable profile-continuity
persistence by default. `client.consent(false)` clears event consent and persistence consent, purges
SDK queues, and clears SDK-managed durable profile-continuity storage while leaving active in-memory
state available until the app resets or tears down the client.

Use `client.consent(events:persistence:)` when event emission and durable profile continuity need
separate policy decisions. For example, this call allows events while keeping profile continuity
session-only:

```swift
client.consent(events: true, persistence: false)
```

`client.consent(events: false)` withdraws event consent and purges SDK queues, but it does not clear
persistence consent unless `persistence: false` is also passed.

Read `client.state.consent` for event consent and `client.state.persistenceConsent` for durable
profile-continuity persistence consent. For cross-SDK consent guidance, see
[Consent management in the Optimization SDK Suite](/personalization/optimization-sdk/consent-management-in-the-optimization-sdk-suite/).

## Entry optimization boundary

iOS entry resolution follows the cross-SDK data model, cross-content selection, empty-variant, and
baseline-fallback behavior described in
[Entry optimization and variant resolution](/personalization/optimization-sdk/entry-personalization-and-variant-resolution/).
The iOS direct method is synchronous. Its `Contentful.Entry` overload returns the baseline or
selected variant as a `CTEntry`:

```swift
let result = client.resolveOptimizedEntry(
    baseline: entry,
    selectedOptimizations: client.selectedOptimizations
)

let resolvedEntry = result.entry
let selectedOptimization = result.selectedOptimization

switch resolvedEntry.contentTypeId {
case "hero" where resolvedEntry.hasField("headline"):
    let headline: String? = resolvedEntry.getField("headline")
    renderHero(headline)
case "cta" where resolvedEntry.hasField("label"):
    let label: String? = resolvedEntry.getField("label")
    renderCta(label)
default:
    let title: String? = resolvedEntry.getField("title")
    renderPage(title)
}
```

`contentTypeId` reads Contentful's `sys.contentType.sys.id` discriminant. It identifies the entry
shape but does not validate its fields. `hasField` checks field presence, and `getField<T>` reads a
field as the requested Swift type. Dictionary-based callers receive `[String: Any]` instead and can
read the same discriminant from the nested `sys` fields.

Omit `selectedOptimizations` when you want direct resolution to use the current bridge and client
state. Pass an explicit snapshot for locked UIKit screens or custom abstractions that must keep the
same variant until the app deliberately redraws or reloads that view.

SwiftUI `OptimizedEntry` adds variant locking, live updates, and interaction tracking around the
same resolver.

## Tracking mechanics

The iOS SDK emits mobile screen events, custom business events, and Contentful entry interaction
events:

| Event type | SwiftUI path                   | UIKit path                                                            |
| ---------- | ------------------------------ | --------------------------------------------------------------------- |
| Screen     | `.trackScreen(name:)`          | `client.trackCurrentScreen(...)`                                      |
| Event      | App-owned event handlers       | `client.track(event:properties:)`                                     |
| Entry view | `OptimizedEntry` view tracking | `ViewTrackingController` or `client.trackView(TrackViewPayload(...))` |
| Entry tap  | `OptimizedEntry` tap tracking  | `client.trackClick(TrackClickPayload(...))`                           |

SDK-managed entry interaction tracking uses these defaults:

* Entry view and tap tracking are enabled by default.
* A view session qualifies after 1 second at 10% visibility and emits one start interaction.
* While the entry remains visible, the SDK measures duration without periodic interaction timers or
  emissions.
* When a qualified session ends, the SDK emits one end interaction with the same `viewId` and the
  final duration. A session that ends before the dwell threshold emits nothing.
* Backgrounding, disappearance, and unmounting end and reset active view sessions. On iOS/UIKit,
  `pause()` ends a qualified session instead of preserving it. Foreground resume re-evaluates
  visibility and starts a fresh session when the entry is still eligible.

The 10% visibility threshold and 1-second dwell are fixed. Wrap scrollable SwiftUI content in
`OptimizationScrollView` when view timing needs an accurate viewport.

UIKit does not have automatic component visibility tracking. UIKit apps compute visibility and
duration through their own table, collection, or view-controller callbacks. Use
`ViewTrackingController` to apply the SDK visibility timing model, or call
`client.trackView(TrackViewPayload(...))` directly when an app-owned abstraction already computes
view duration. Call `client.trackClick(TrackClickPayload(...))` from UIKit control actions or
gesture recognizers.

## Live updates and preview behavior

SwiftUI `OptimizedEntry` locks to the first resolved variant by default. Locking prevents content
from changing while a visitor is reading it. Enable live updates when a component needs to react to
profile changes or preview overrides without a reload.

SwiftUI live-update precedence is:

| Preview panel | Global default | Per-entry override | Result |
| ------------- | -------------- | ------------------ | ------ |
| Open          | Any            | Any                | Live   |
| Closed        | `true`         | `nil`              | Live   |
| Closed        | `false`        | `true`             | Live   |
| Closed        | `true`         | `false`            | Locked |
| Closed        | `false`        | `nil`              | Locked |

Opening the preview panel sets `client.isPreviewPanelOpen`. SwiftUI `OptimizedEntry` treats that
state as a live-update override, so audience and variant overrides apply immediately in those
components. When the panel closes, entries that return to locked mode snapshot the current
`client.selectedOptimizations` value and keep the previewed variant. Entries whose global default or
per-entry override remains live continue to follow `client.selectedOptimizations`.

UIKit apps choose their own live-update policy. Subscribe to `client.$selectedOptimizations`,
`client.$isPreviewPanelOpen`, or `client.$previewState` and redraw views for live behavior, or keep
a selected-optimizations snapshot for locked behavior. Treat `client.isPreviewPanelOpen` as a reason
to redraw in live mode while previewing; the SDK does not automatically rebuild UIKit views.

## Offline and app lifecycle delivery

The SDK monitors network reachability. On iOS/UIKit, it also observes app lifecycle events. No
configuration is required for the default offline path, and queueing and flushing use the same
event-delivery model for SwiftUI and UIKit integrations.

Default delivery behavior:

* Experience events queue in memory when the device is offline.
* Core caps the offline Experience queue at 100 events by default. When the queue is full, it drops
  the oldest offline events before accepting the next event.
* Insights events batch in memory by profile and flush periodically or when the batch reaches the
  Core batch threshold.
* Queued events flush when connectivity returns. On iOS/UIKit, the SDK also performs an online
  best-effort flush when the app moves toward the background; if the device remains offline, queued
  events wait for reconnect.
* Queues do not survive process death. Keep one `OptimizationClient` alive for the app or scene
  lifetime to preserve queued events across transient connectivity changes.

Periodic Insights batch flushing is a queue-delivery schedule. It does not emit additional view
interactions while an entry session remains visible.

Use `OptimizationConfig(queuePolicy:)` when production behavior needs non-default delivery
constraints. `QueuePolicy.offlineMaxEvents` changes the Experience offline cap, `QueueFlushPolicy`
configures retry timing, backoff, jitter, failure thresholds, and circuit-open timing, and queue
callbacks report offline drops, flush failures, circuit-open events, and recovery. Call
`client.flush()` only for deliberate app-owned checkpoints; reconnect flushing and iOS/UIKit
background flushing already perform best-effort delivery.

## Related documentation

* [Optimization iOS SDK README](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/packages/ios/ContentfulOptimization/README.md) - Package
  installation, quick start, and published package status.
* [Consent management in the Optimization SDK Suite](/personalization/optimization-sdk/consent-management-in-the-optimization-sdk-suite/) -
  Cross-SDK consent, persistence consent, allow-list, and withdrawal policy guidance.
* [Locale handling in the Optimization SDK Suite](/personalization/optimization-sdk/locale-handling-in-the-optimization-sdk-suite/) -
  How SDK Experience/event locales relate to Contentful entry locales.
* [Entry optimization and variant resolution](/personalization/optimization-sdk/entry-personalization-and-variant-resolution/) -
  Contentful entry contract, variant fallback behavior, and local resolution mechanics.
* [Core state management](/personalization/optimization-sdk/core-state-management/) - Shared state, consent, persistence, event
  queues, and observable mechanics used through the native bridge.
* [Integrate the Optimization iOS SDK in a SwiftUI app](/personalization/optimization-sdk/integrate-the-optimization-ios-sdk-in-a-swiftui-app/) -
  SwiftUI setup flow for `OptimizationRoot`, `OptimizedEntry`, screen tracking, and preview panel
  mounting.
* [Integrate the Optimization iOS SDK in a UIKit app](/personalization/optimization-sdk/integrate-the-optimization-ios-sdk-in-a-uikit-app/) -
  UIKit setup flow for direct `OptimizationClient` usage, manual entry resolution, tracking, and
  preview panel mounting.
* [iOS reference implementation](https://github.com/contentful/optimization/blob/optimization-swift-v2.0.1/implementations/ios-sdk/README.md) - Native iOS validation
  app with SwiftUI and UIKit shells.