e-volv
Docs menu

iOS and macOS

EvolveLogs (Swift package github.com/e-volv/logs-swift) is the e-volv Launch SDK for iOS 15+ and macOS 13+. It is a client SDK: with a public (evk_pub_…) key it receives evaluated values for the current context — never targeting rules — so a flag check is synchronous, never throws and never blocks on the network. A server (evk_…) key is refused at init: it must never ship inside a binary. Values are cached in one on-device envelope per key, refreshed by a poll with a foreground hook, and exposure events ride the same URLSession as your logs. The behaviour is pinned by the Launch SDK contract and both of its conformance suites run in CI.

Install

Add the Swift package from github.com/e-volv/logs-swift in Xcode — File → Add Package Dependencies — or declare it in your own package manifest. SwiftPM names the package after the repository, so the product is referenced from logs-swift:

swift
// Package.swift — dependencies
.package(url: "https://github.com/e-volv/logs-swift", from: "0.1.0")

// then, on your app target:
.target(name: "MyApp", dependencies: [
    .product(name: "EvolveLogs", package: "logs-swift"),
])

With CocoaPods, add the pod to your Podfile instead:

ruby
# Podfile
pod 'EvolveLogs', '~> 0.1'

Native API

swift
import EvolveLogs

var flagsOptions = FlagsOptions()
flagsOptions.initialContext = ["targetingKey": user.id, "plan": user.plan]

EvolveLogs.initialize(EvolveLogsOptions(
    key: "evk_pub_…",   // a public client key — never a server (evk_) key
    url: "https://api.e-volv.io/api/public/v1/logs",
    service: "acme-shop",
    environment: "production",
    release: "1.4.2",
    flags: flagsOptions
))

let flags = EvolveLogs.flags
await flags.ready() // optional: true once values for this context arrived

if flags.bool("checkout.new", default: false) {
    renderNewCheckout()
}

// The full detail when you need the variant and the reason:
let d = flags.detail("checkout.banner", default: .string("default"))
// d.value (JSONValue), d.variant ("on" | "off" | "control" | …),
// d.reason ("RULE:0" | "ROLLOUT" | "TARGET_MATCH" | "DEFAULT" | "FLAG_NOT_FOUND" | …)

// On login/logout, re-identify: values for the new context are fetched
// and cached under the same key. The Bool reports whether the values now
// served are for this context.
let served = await flags.identify(["targetingKey": nextUser.id, "plan": nextUser.plan])

Initialize from UIApplicationDelegate.application(_:didFinishLaunchingWithOptions:) (or your SwiftUI .init). Before initialize — or with flags disabled — EvolveLogs.flags is a disabled instance: reads return defaults and no network or disk I/O happens. Evaluation methods: bool, string, number, json, detail and typedDetail — all synchronous, all safe. A flag absent from the served values returns your default with reason FLAG_NOT_FOUND; a value of the wrong type returns your default with TYPE_MISMATCH. The detail answer is an Evaluationvalue (JSONValue: .bool, .number(Double), .string, .array, .object, .null), variant and reason. Swift has no mode option: the client always polls unless offline is set, and the flags.mode property reports "poll" or "offline".

Options

Flags are configured with the flags value of EvolveLogsOptions — a FlagsOptions struct whose properties all default per the contract’s client defaults:

OptionDefaultMeaning
flags.enabledtrueStart the flags client. False gives a defaults-only instance that performs no I/O.
flags.urlnilBase URL ending in /api/public/v1/flags; defaults to the origin of the Observer url option, else the e-volv cloud.
flags.offlinefalseNever contact the control plane: bootstrap or cached values, then defaults only.
flags.pollIntervalSeconds60 (min 15)Poll period — the first fetch runs immediately, then the poll repeats. Client SDKs never stream.
flags.cacheEnabledtrueKeep the last-known values on disk: one envelope per key, written atomically and excluded from iCloud backups.
flags.cacheDirectoryqueueDirectory/flagsWhere the values envelope is written (beside the Observer disk queue by default).
flags.bootstrapnilA bundled values map served before the first fetch completes.
flags.initialContext[:]The context the first fetch uses, until identify replaces it.
flags.exposuresEnabledtrueRecord an exposure on each evaluation of a flag present in the values.
flags.exposureSampleRate1Probability an evaluation is recorded; sent as sampleRate.
flags.exposureDedupeWindowSeconds300Suppress repeats of the same (flagKey, variant, contextKind, subject) within the window; suppressed counts ride the next exposure.
flags.sendAttributesfalseSend context attributes with exposures (private attributes are always removed).
flags.privateAttributes[]Attribute names never sent anywhere.

What the app sends — and what it keeps

  • Every request carries Authorization: Bearer with the key, x-evolve-app-id (your bundle identifier) and a generated, persisted x-evolve-install-id — the control plane checks the key’s app-id allowlist. The SDK names itself with the e-volv-logs-swift/<version> user agent.
  • The context travels as ?context= JSON, capped at 4,096 UTF-16 characters: attributes are dropped largest-first — never key, kind or targetingKey — and the trim is logged once.
  • privateAttributes are removed before anything is sent. A rule that targets a private attribute can never match a client key — the attribute simply is not there.
  • The cache is one file per key (evolve-flags-<first 12 hex of sha1(key)>.json) kept beside the Observer disk queue and excluded from iCloud backups, holding the served values and a sha1 fingerprint of the fitted context — no rules, no personal data beyond what evaluation needed.

identify returns whether the values now served are for the new context. While its fetch is in flight, reads keep answering from the previous context’s values; a cache written for another context is likewise served only until the first fetch for the new context completes, then overwritten.

Install check and lifecycle

swift
// Install check: is this key bound, and to which environment?
let ping = await flags.verify()
// PingResult: environment, keyKind ("client"), flags (count), etag
// nil when the control plane did not answer.

flags.ready(timeout:) waits (default 5 seconds) for values confirmed by the control plane for the current context — bootstrap and cache serve reads immediately, so this only gates on the network answer. flags.lastUpdatedAt reports when the held values were last confirmed, and flags.onChange { keys in } notifies with the keys whose values changed — keep the returned FlagsSubscription and call cancel() to detach. flags.refresh() forces a coalesced fetch now; flags.refreshIfStale() fetches only when the last fetch is older than 15 s (the hook the SDK calls on every app foreground). flags.flushExposures() drains pending exposures, 1,000 per request.

swift
// Stop the poller and flush pending exposures. Idempotent; the final
// flush runs detached, so closing never parks the calling thread (safe
// from applicationWillTerminate). client.shutdown() does this too.
flags.close()

Troubleshooting by status code

Evaluation never breaks because of the control plane — it degrades. What each response means for the SDK (contract §2.6):

ResponseSDK behaviour
401Key invalid or revoked. The SDK keeps serving held values or defaults, logs once, and retries bootstrap every 5 minutes.
403 — lacks the scope flags:readFlags are disabled for this key with one warning; telemetry is unaffected. No retry until re-init.
403 — origin or app id refusedOne warning naming the environment&rsquo;s Access action, then requests pause for 5 minutes while held values or defaults keep serving.
404Launch is not enabled for the workspace, or the key is not bound to an environment. Same handling as 401.
429Honours Retry-After (seconds; 30 s when the header is absent).
5xx, timeout, network errorOne log per outage — &ldquo;control plane unreachable, serving last known values&rdquo; — while the poll keeps its cadence.
rulesetVersion > 2Payload ignored with one warning (&ldquo;upgrade e-volv-logs-swift&rdquo;); the last held values keep serving.

Guarantees

  • A flag check is local and synchronous — bounded by CPU, not the network; ready(timeout:) and identify are the only calls that wait.
  • An unreachable or slow control plane never changes an answer mid-flag: the last held values (or the on-device cache, or your defaults) keep serving while the SDK applies the status policy above.
  • Cold start reads the cache instantly and swaps to fresh values without a flicker for unchanged flags; a cache for another context is served only until the first fetch for the new context completes; corrupt or unreadable caches are ignored and overwritten.
  • Exposures are sampled, de-duplicated (default 300 s window) and capped (10,000 buffered, oldest dropped), posted 1,000 per request through the same URLSession as your logs and requeued on failure — and a FLAG_NOT_FOUND or TYPE_MISMATCH read records nothing.

The flags client shares its key, app id and install id with the e-volv Observer iOS SDK — one init wires both. The SDKs index lists every platform, the e-volv Launch overview explains the model, and the HTTP API page documents the wire format the SDK speaks.