Shipeasy
SDKsReferenceSwift

Agent skill

Installable LLM skill for the Swift SDK (configure() + Client(user), evaluate, experiment + track, testing).

An installable agent skill for the Swift SDK. Fetch it with shipeasy docs skill --sdk swift (--install writes it to your agent skills dir), or copy it below — the YAML frontmatter installs with it.

---
name: shipeasy-swift
description: Use Shipeasy from a native iOS/macOS/tvOS/watchOS app in Swift — feature flags, dynamic configs, kill switches, A/B experiments, metric tracking, and see() error reporting. The native client SDK uses a public client key (pk_…, safe to embed), configureClient() once at launch returns a ShipeasyClient actor, identify() binds the device user and refreshes assignments, getFlag/getConfig/getKillswitch serve cached reads, and universe(name).assign() returns an experiment Assignment. Persists the device anonymous_id across launches so bucketing is stable. SwiftPM (iOS 15+/macOS 12+/tvOS 15+/watchOS 8+).
---

# Shipeasy Swift SDK (native client)

SwiftPM SDK for **shipped iOS / macOS / tvOS / watchOS apps** (iOS 15+ / macOS 12+
/ tvOS 15+ / watchOS 8+). It uses a **public client key** (`pk_…`, safe to embed),
evaluates the device user server-side over `POST /sdk/evaluate`, and serves cheap
local reads from the cached response. `ShipeasyClient` is a Swift `actor`, so its
methods are `async`. There is **no server surface, no OpenFeature provider, and no
i18n** in this SDK.

> The documented surface is exactly **`configureClient(clientKey:)`** (once, at
> launch) and the **`ShipeasyClient`** it returns, plus the package-level `see()`
> family. For deeper docs, fetch any page/snippet from the manifest at
> <https://shipeasy-ai.github.io/sdk-swift/manifest.json> (raw URLs below).

## Install

```swift
// Package.swift
.package(url: "https://github.com/shipeasy-ai/sdk-swift.git", from: "3.0.0"),
```

```swift
import Shipeasy
```

## Configure once at launch, then identify + read

```swift
import Shipeasy

// Once at app launch (SwiftUI App init / AppDelegate). PUBLIC client key (pk_…),
// safe to embed. First-config-wins; returns + registers the process-global client.
let client = configureClient(clientKey: "pk_live_…")

// Bind the user ([:] for a logged-out visitor; again on login). Attaches the
// persisted device anonymous_id automatically so bucketing is stable across
// cold starts. Awaiting the first identify guarantees the first reads see assignments.
await client.identify(["user_id": "u_123", "plan": "pro"])

// Anywhere later, fetch the configured client with shipeasyClient():
let on = await shipeasyClient()?.getFlag("new_checkout") ?? false
```

Reference: <https://shipeasy-ai.github.io/sdk-swift/pages/installation.md> ·
<https://shipeasy-ai.github.io/sdk-swift/pages/configuration.md>

## Evaluate (cached reads)

```swift
let client = shipeasyClient()!
let enabled = await client.getFlag("new_checkout")                 // Bool
let on      = await client.getFlag("new_checkout", default: true)  // default only when unevaluable
let cfg     = await client.getConfig("billing_copy", default: ["headline": "Hi"]) // Any?
let killed  = await client.getKillswitch("panic_button")           // true = killed
// Named switch: getKillswitch(name, switchKey:) — an unconfigured key falls back
// to the kill switch's top-level value.
```

Reads serve the cached `/sdk/evaluate` response (no per-call network, any thread).
Before the first identify they return the supplied defaults. Reference:
<https://shipeasy-ai.github.io/sdk-swift/pages/flags.md>

## Identity lifecycle

```swift
await client.identify(["user_id": "u_123"])   // launch / login / attrs changed
await client.reset()                          // logout: keep device anon id, drop user_id
await client.refreshAssignments()             // re-evaluate current user (pick up a new flag)
let id = await client.anonymousId             // stable, persisted device bucketing id
```

## Experiments + track

```swift
let client = shipeasyClient()!
// A universe is a mutual-exclusion pool → the unit lands in ≤1 experiment.
let a = await client.universe("checkout").assign()            // auto-logs one exposure when enrolled
// a.name: String?, a.group: String?, a.enrolled: Bool
let color = a.get("button_color", "blue")                    // variant override ?? universe default ?? fallback
await client.track("purchase", properties: ["amount": 49])    // conversion (fire-and-forget)
```

Reference: <https://shipeasy-ai.github.io/sdk-swift/pages/experiments.md> · track
snippet <https://shipeasy-ai.github.io/sdk-swift/snippets/metrics/track.md>

## Testing (hermetic — no network, no UserDefaults)

Build a `ShipeasyClient` directly with an in-memory `AnonymousStore` + a stub
`Transport` returning a canned `/sdk/evaluate` body; `await identify`, then assert.

```swift
final class MemStore: AnonymousStore, @unchecked Sendable {
    private var map: [String: String] = [:]
    func get(_ key: String) -> String? { map[key] }
    func set(_ key: String, _ value: String) { map[key] = value }
}

let transport: ShipeasyClient.Transport = { req in
    let body: [String: Any] = ["flags": ["new_ui": true]]
    let data = try JSONSerialization.data(withJSONObject: body)
    return (data, HTTPURLResponse(url: req.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)
}

// isNetworkEnabled: true forces the network on (the SDK is offline by default
// outside production); isTrackingEnabled: false keeps the telemetry beacon off.
let client = ShipeasyClient(clientKey: "pk_test", isNetworkEnabled: true,
                            isTrackingEnabled: false, store: MemStore(), transport: transport)
await client.identify(["user_id": "u1"])
_ = await client.getFlag("new_ui", default: false)   // true
// resetClientConfig() drops the process-global client between tests (tests only).
```

Reference: <https://shipeasy-ai.github.io/sdk-swift/pages/testing.md>

### Local overrides

Force a value at runtime (tests + the native devtools overlay), winning over the
cached assignment and skipping telemetry/exposure. All `async`:

```swift
await client.overrideFlag("new_ui", true)                      // getFlag → true
await client.overrideConfig("theme", ["accent": "green"])      // getConfig → value; nil forces "absent"
await client.overrideExperiment("checkout", group: "treatment", // force a variant…
                                params: ["headline": "Forced"]) // …params layer over universe defaults
await client.clearOverrides()                                  // or removeOverride(kind:name:)
```

## Error reporting — see()

```swift
do {
    try chargeCard(order)
} catch {
    see(error).causesThe("checkout").to("use cached prices")
    // extras fold into the terminal — NEVER .causesThe(x).extras([...]).to(y),
    // which splits the consequence sentence in half:
    see(error).causesThe("checkout").to("use cached prices", extras: ["order_id": order.id])
}

seeViolation("large query").causesThe("search results").to("be trimmed")
controlFlowException(error).because("expected — token expiry is normal") // reports nothing
```

`to(_:)` is the terminal (nothing sends without it) and returns `Void`, so
extras cannot trail it — `to(_:extras:)` folds them in. Package-level `see(_:)` /
`seeViolation(_:)`
dispatch through the configured client (side `"client"`).
Reference: <https://shipeasy-ai.github.io/sdk-swift/pages/error-reporting.md> ·
snippet <https://shipeasy-ai.github.io/sdk-swift/snippets/ops/see.md>

## Notes

- **No OpenFeature provider** in Swift — use `getFlag`. Reference:
  <https://shipeasy-ai.github.io/sdk-swift/pages/openfeature.md>
- **No i18n** in the native client — localize with the platform's own tooling
  (String Catalogs / `Localizable.strings`). Reference:
  <https://shipeasy-ai.github.io/sdk-swift/pages/i18n.md>
- **Persisted anon id** is the point of the SDK: a custom `AnonymousStore` (Keychain
  / app-group / tests) backs it via `configureClient(clientKey:store:)`; also
  `privateAttributes` (stripped from `track`/`see`), `refreshAssignments`,
  `anonymousId`. Reference:
  <https://shipeasy-ai.github.io/sdk-swift/pages/advanced.md>
Was this page helpful?
Updated July 25, 2026