iOS
Use the FounderHQ iOS SDK.
Capture screens, app lifecycle, and your own events from a Swift app.
Before you start
You need a publishable key (fhq_pk_...) for the brand you want to
measure. See Getting started.
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:
pod 'FounderHQEvents', '~> 0.8.0'You can also install directly from the Git release:
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.
import FounderHQEvents
let events = FounderHQEvents(apiKey: "fhq_pk_XXXX")Pass a configuration when you want to change the defaults:
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
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:
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:
PricingView()
.founderHQScreen("Pricing", client: events)Call events.screen("Pricing") directly anywhere else.
Identify a contact
events.identify("user_42", properties: ["email": "jane@acme.com"])Change identity and account in one call:
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
events.setAccount("workspace_123", properties: ["plan": "growth"])
events.setAccountProperties(["seats": 12])
events.clearAccount() // resetAccounts() does the samesetAccount 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.
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.
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.
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 |
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 for the wire format, the reserved event names, and the campaign properties a deep link can carry.