# iOS (https://www.getfounderhq.com/docs/analytics/sdks/ios)

Capture screens, app lifecycle, and your own events from a Swift app.

<Callout title="Before you start">
You need a publishable key (`fhq_pk_...`) for the brand you want to
measure. See [Getting started](/analytics/getting-started).
</Callout>

## Install

In Xcode, add `https://github.com/FounderHQ/founderhq-events-ios` with
Swift Package Manager and select version **0.8.0** or later.

For CocoaPods:

```ruby
pod 'FounderHQEvents', '~> 0.8.0'
```

You can also install directly from the Git release:

```ruby
pod 'FounderHQEvents', :git => 'https://github.com/FounderHQ/founderhq-events-ios.git', :tag => 'v0.8.0'
```

The SDK needs iOS 15 or macOS 12, and Swift 5.9.

## Initialize

Create one client and keep it for the life of the app.

```swift
import FounderHQEvents

let events = FounderHQEvents(apiKey: "fhq_pk_XXXX")
```

Pass a configuration when you want to change the defaults:

```swift
let events = FounderHQEvents(
    apiKey: "fhq_pk_XXXX",
    configuration: .init(
        personProfiles: .identifiedOnly,
        captureScreens: true
    )
)
```

The client captures UIKit screens and application lifecycle on its own, and
records the session start.

## Capture events

```swift
events.capture("trial.started", properties: ["plan": "growth"])
```

`capture` returns at once and does the work in the background. Every
writing method has an `...AndWait` twin that you can `await`:
`captureAndWait`, `identifyAndWait`, `screenAndWait`, `setAccountAndWait`,
`clearAccountAndWait`, `setAccountPropertiesAndWait`,
`setPersonPropertiesAndWait`, `registerAndWait`, `registerOnceAndWait`,
`unregisterAndWait`, `optInAndWait`, `optOutAndWait`, and `resetAndWait`.
`readyForCapture()` resolves once startup and the queued calls are done.

Record a deep link when your app opens from one:

```swift
events.captureDeepLink(url)
```

`captureDeepLink` drops the query and fragment, and keeps the campaign
parameters it recognizes.

## Track screens

UIKit screens are captured for you. In SwiftUI, mark the view:

```swift
PricingView()
    .founderHQScreen("Pricing", client: events)
```

Call `events.screen("Pricing")` directly anywhere else.

## Identify a contact

```swift
events.identify("user_42", properties: ["email": "jane@acme.com"])
```

Change identity and account in one call:

```swift
events.identify(
    "user_42",
    properties: ["email": "jane@acme.com"],
    account: FounderHQAccountContext(key: "workspace_123")
)
```

Update contact properties on their own with
`setPersonProperties(_:setOnce:)`. With the default `identifiedOnly` mode,
changes made before `identify` stay on the device and go out with the
identify call.

Call `reset()` on logout. It clears the identity and the account.

## Accounts

```swift
events.setAccount("workspace_123", properties: ["plan": "growth"])
events.setAccountProperties(["seats": 12])
events.clearAccount() // resetAccounts() does the same
```

`setAccount` also takes a `FounderHQAccountContext`, which carries `key`,
`properties`, and `contextToken`. Set `account` in the configuration when
the first automatic event must already carry it. An account change rotates
the account span only. The person session and queued events are untouched.
See [Accounts and groups](/analytics/concepts/accounts-and-groups).

## Consent

| You call | What happens |
| --- | --- |
| `optIn()` | Capture is on |
| `optOut()` | Capture stops |
| `isOptedOut()` | Returns `true` while capture is off |

Set `optOutByDefault: true` in the configuration to start silent until the
person agrees. See
[Consent and privacy](/analytics/concepts/consent-and-privacy).

The SDK never collects advertising identifiers.

## Purchases

The client can tie a StoreKit or RevenueCat purchase to the person who made
it.

| Method | What it does |
| --- | --- |
| `purchaseAttribution()` | Returns the identifiers to pass to the store or to RevenueCat |
| `preparePurchase(source:)` | Records intent before checkout, and returns the purchase context |
| `purchase(_:options:)` | Runs a StoreKit purchase with the context already attached |
| `observePurchase(_:prepared:)` | Reports the purchase that followed |
| `claimSubscription(_:confirmation:)` | Moves future revenue of an existing subscription |

The full flow, including what each store needs, is on
[Mobile purchase claims](/analytics/revenue/mobile-purchase-claims).

## Configuration

`FounderHQEventsConfiguration` takes these values.

| Value | Type | Default | What it does |
| --- | --- | --- | --- |
| `host` | `URL` | `https://i.getfounderhq.com` | Where events are sent |
| `flushAt` | `Int` | `20` | Sends a batch once this many events are queued |
| `flushInterval` | `TimeInterval` | `10` | Sends a batch on this timer, in seconds |
| `personProfiles` | `FounderHQPersonProfiles` | `.identifiedOnly` | When contact property changes are applied |
| `optOutByDefault` | `Bool` | `false` | Starts opted out |
| `captureLifecycle` | `Bool` | `true` | App opened and backgrounded events |
| `captureScreens` | `Bool` | `true` | UIKit screen events |
| `captureSessions` | `Bool` | `true` | Session start events |
| `captureInstallUpdates` | `Bool` | `true` | App installed and updated events |
| `remoteConfig` | `Bool` | `true` | Reads capture settings you set in FounderHQ |
| `account` | `FounderHQAccountContext?` | `nil` | Installs account context before the first event |
| `purchasePrepareTimeout` | `TimeInterval` | `3` | How long `preparePurchase` waits before checkout goes ahead offline |
| `captureElementInteractions` | `Bool` | `false` | Taps and rage taps. See [Element interactions](#element-interactions) |
| `capturePushNotificationOpened` | `Bool` | `true` | Records when someone taps one of your notifications |
| `tracingHeaders` | `[String]?` | `nil` | Hostnames whose requests carry the session id |
| `maxQueueSize` | `Int` | `1000` | Events kept offline before the oldest are dropped |
| `eventTTL` | `TimeInterval` | `86400` | How long an unsent event may wait, in seconds |
| `maxRetries` | `Int` | `5` | Times a failed event is retried |
| `debug` | `Bool` | `false` | Logs what the SDK drops or refuses |

`FounderHQEventsDependencies` replaces the clock, UUID source, storage,
transport, platform facts, and screen-capture installer. Use it in tests.

## Sending

| Method | Returns | What it does |
| --- | --- | --- |
| `flush()` | `Bool` | Sends queued events now, and tells you whether the queue drained |
| `close()` | — | Flushes, stops the timer, and removes the lifecycle observers |
| `getDistinctId()` | `String` | The current identity |
| `getSessionId()` | `String` | The current session |

## Element interactions

Set `captureElementInteractions` to `true`. The SDK then records
`$autocapture` when someone taps a control, and `$rageclick` when they tap the
same control three times in a second. Both are off by default.

Each event carries the control and up to four of its parents.

| Stored | Not stored |
| --- | --- |
| The class, accessibility identifier, and accessibility label | Anything a person typed |
| The action, the enabled state, and the selected state | Tap coordinates |
| The title a `UIButton`, `UIBarButtonItem`, or `UISegmentedControl` shows | The contents of a `UITextField`, `UITextView`, or `UISearchBar` |

Text fields, text views, and search bars are skipped. A tap on one records
nothing at all.

Titles are cut to 255 characters. Numbers that look like a card number or a
social security number are removed from them.

## Remote config

The client reads your capture settings from FounderHQ at startup, caches
them in its storage, and applies the cached copy on the next launch.

| Key | What it turns on and off |
| --- | --- |
| `capture_sessions` | Session start events |
| `capture_screens` | Screen events |
| `capture_lifecycle` | App opened and backgrounded events |
| `autocapture` | Taps and rage taps |
| `capture_rageclicks` | Rage taps only. Taps are still recorded |

Your settings in FounderHQ win. They can turn capture off for every install,
and on for an app that shipped with the wrong value. You cannot rebuild an app
people have already installed, so the dashboard decides. The values in your
configuration apply until your settings arrive.

Events already queued under a key that turns off are dropped before they
leave the device.

Call `refreshRemoteConfig()` to fetch them again while the app runs. Call
`applyRemoteConfig(_:)` to apply settings you supply yourself. Set
`remoteConfig: false` in the configuration to skip the request.

## Next

Read the [protocol reference](/analytics/protocol-reference) for the wire
format, the reserved event names, and the campaign properties a deep link
can carry.
