# iOS SDK

CohorlySwift is a standalone Swift Package for Cohorly, supporting iOS 15+ and macOS 12+. It uses URLSession for transport, batches events, and persists its queue to UserDefaults by default.

## Install

`CohorlySwift` is distributed as a Swift Package - add it from the Cohorly
package repository.

### Xcode

File > Add Package Dependencies..., enter the repo URL, pick a version
rule, and add the **Cohorly** library product to your app target.

### Package.swift

```swift
dependencies: [
    .package(url: "https://github.com/Gitarcitano/cohorly-swift", from: "0.1.0"),
],
targets: [
    .target(
        name: "YourApp",
        dependencies: [.product(name: "Cohorly", package: "cohorly-swift")]
    ),
]
```

## Initialize

Call `Cohorly.initialize` once near app launch. It configures and returns
the shared instance (`Cohorly.shared`). `token` is optional - omit it if
you don't use per-project tokens.

```swift
import Cohorly

let cohorly = Cohorly.initialize(token: "YOUR_PROJECT_TOKEN")

cohorly.register(["app_version": "1.2.0"])
cohorly.track("Screen Viewed", properties: ["screen": "Home"])

cohorly.identify("user_123")
cohorly.people.set(["plan": "pro"])

// Manually flush (also happens automatically every 5s or every 20 events).
await cohorly.flush()
```

## API

| Member                                                          | Notes                                                                                                                                                                         |
| --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Cohorly.initialize(apiHost:token:...)`                         | Configures and returns `Cohorly.shared`. Also accepts `transport`, `storage`, `flushInterval`, `flushAt`.                                                                     |
| `track(_:properties:)`                                          | Queue an event.                                                                                                                                                               |
| `identify(_:)`                                                  | Set the distinct id. On the first call after being anonymous, also posts `/alias` linking the previous anonymous id (best effort, not retried). No-op if the id is unchanged. |
| `reset()`                                                       | Clears identity, super properties, and un-flushed events. Call this on logout - see [Identity](/reference/data-model#identity).                                               |
| `register(_:)` / `unregister(_:)`                               | Super properties merged into every event.                                                                                                                                     |
| `people.set / setOnce / increment / unset / deleteUser`         | Profile updates sent to /engage.                                                                                                                                              |
| `flush() async`                                                 | Manually flush; resolves once any auto-triggered flush completes.                                                                                                             |
| `currentDistinctId: String`                                     | Current distinct id.                                                                                                                                                          |
| `getFeatureFlag(_:fallback:)` / `isFeatureEnabled(_:fallback:)` | Read a flag's variant/enabled value; never hits the network.                                                                                                                  |
| `getFeatureFlagDetails(_:fallback:)`                            | The value plus `source` (`network` / `persistence` / `fallback`) and cache metadata.                                                                                          |
| `reloadFeatureFlags() async` / `onFeatureFlags(_:)`             | Re-evaluate flags / subscribe to updates.                                                                                                                                     |

## Feature flags

Flags are evaluated on `initialize` (opt out with `loadFeatureFlags: false`),
on `identify()` and on `reset()`, and cached in storage scoped by distinct id;
reads never hit the network.

`flagPersistence` (default `.persistenceUntilNetworkSuccess`) controls how
reads resolve against the persisted cache:

| Policy                            | Behavior                                                                                                                                                                                                                                                                                     |
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `.networkOnly`                    | Never reads nor writes the cache on the serving path (identity changes still remove it - that is hygiene, not serving). Flags come only from network responses this session; a failed evaluation serves the fallback.                                                                        |
| `.networkFirst`                   | Prefers the network, but reads are synchronous, so a valid cache (inside the TTL) is served immediately like `.persistenceUntilNetworkSuccess`. Strict about freshness: a cache past the TTL is dropped, never served.                                                                       |
| `.persistenceUntilNetworkSuccess` | Serves a valid cache immediately, so startup doesn't flicker; the first network success takes over and re-persists. A failed evaluation keeps the cache serving. Trades freshness for offline availability: an expired cache still serves while inside `Cohorly.flagCacheMaxStale` (7 days). |

The cache carries a `stored_at` timestamp and expires after `flagCacheTTL`
(default **24 hours** - `Cohorly.defaultFlagCacheTTL`, the Mixpanel reference
default shared by every Cohorly SDK). A legacy cache with no timestamp cannot
be dated, so it is served indefinitely rather than expired - the
graceful-upgrade path preserving the pre-TTL behavior - until the next
successful reload rewrites it in the stamped format. `identify()` and
`reset()` clear the previous identity's flags before fetching the new
identity's, so values never leak across users - `onFeatureFlags` listeners
are notified of the clear with an empty map, then again when the new
identity's flags land.

```swift
let variant = cohorly.getFeatureFlag("checkout-copy", fallback: "control")
let details = cohorly.getFeatureFlagDetails("checkout-copy")
// details.source: .network | .persistence | .fallback
// details.enabled / details.payload; a fallback result reports
// enabled = truthiness of the fallback and payload = nil
// details.persistedAt / details.ttlMs when source == .persistence
```

A read that resolves to the fallback reports no exposure. Reads of known
flags emit `$feature_flag_called` once per (key, value) per identity, stamped
with `$feature_flag_source` (`"network"` or `"persistence"`) plus
`$feature_flag_persisted_at` and `$feature_flag_ttl_ms` for persistence reads;
the same value served first from persistence and then from the network is
reported once.

## Auto-captured properties

Every event includes:

- `$lib` = `"ios"`
- `$os`, `$os_version`, `$model`
- `$screen_width` / `$screen_height` (via UIKit where available - skipped
  on macOS/watchOS)

Events and profile updates are queued in memory and persisted to storage
(UserDefaults by default) so nothing is lost across relaunch. All mutable
state is confined to a private serial `DispatchQueue`, so the client is
safe to call from any thread.
