iOS SDK (Swift)
For account IDs, backend-verified guests, token expiry, property updates, and logout, see the identity integration guide. The lifecycle APIs documented there require 0.1.4 or later. Native iOS 0.1.3 and earlier do not expose async identify/reset completion.
Production iOS 14+ · Swift 5.9
UserGistFeedback provides authenticated anonymous and identified subjects,
offline queues, consent controls, locally targeted campaigns, surveys,
feature requests, and host-compatible APNs integration.
Install
Add the package from Xcode:
- File → Add Packages…
- Enter
https://github.com/Future-Picnic/usergist-ios.git. - Add the
UserGistFeedbackproduct to your app target.
Or in Package.swift:
dependencies: [
.package(url: "https://github.com/Future-Picnic/usergist-ios.git", from: "0.1.4")
],
targets: [
.target(
name: "MyApp",
dependencies: [.product(name: "UserGistFeedback", package: "usergist-ios")]
)
]Simulator onboarding troubleshooting
Use 0.1.2 or later for startup presentation readiness. This includes the fix for a crash when feedback or survey ratings have
endpoint labels. Existing projects should update their package dependency and
check the version in Package.resolved before rebuilding.
If $app_open does not arrive and SDK diagnostics report Keychain error
-34018 (errSecMissingEntitlement), remove CODE_SIGNING_ALLOWED=NO from the
host build, rebuild with simulator signing enabled, and reinstall/relaunch.
The SDK cannot start authenticated event delivery until it securely saves the
subject session. This applies to anonymous and identified users in either
orientation. Grant analytics consent and verify the API accepts $app_open
before testing campaign delivery.
Initialise
Call UserGist.shared.initialize(...) in your AppDelegate (or the @main App init)
with campaign presentation paused. This lets analytics and networking start
without placing a feedback sheet over your splash or startup navigation.
import UIKit
import UserGistFeedback
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
UserGist.shared.initialize(
writeKey: "rk_live_REPLACE_ME",
environment: .production,
debug: false,
presentationPaused: true
)
return true
}
}Resume campaigns after startup navigation
In your existing ready-content controller, resume after loading and navigation finish. Keep your app's screens and navigation unchanged:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UserGist.shared.resumePresentation()
}For SwiftUI, attach .onAppear { UserGist.shared.resumePresentation() } to the
loaded-content view that replaces the loading branch. Do not attach it to a root
that still displays a splash screen. Do not use a fixed delay or the splash's
viewDidDisappear; neither establishes that the destination is ready.
pausePresentation() defers new feedback, surveys, and in-app messages during
other sensitive flows such as login or checkout. It does not dismiss a campaign
already visible. Call resumePresentation() when the destination is ready;
repeated calls are safe. Consent withdrawal and identity changes invalidate
waiting campaigns. This queue is in memory; it does not survive app termination.
$app_open still verifies the connection while UI is paused. A campaign triggered
by that event waits until presentation resumes. Readiness does not require sign-in,
and no custom verification event is needed.
If the app opens but feedback never appears, verify that the ready screen calls
resumePresentation() and that feedback consent is granted. SDK 0.1.2 or later
is required for these APIs; updating the server does not update the installed SDK.
Init options
| Option | Type | Default | Notes |
|---|---|---|---|
writeKey | String | — | Required. |
environment | Environment | .production | .production, .staging, .development |
apiURL | URL? | nil | Override only for self-hosted. |
debug | Bool | false | Verbose logging. |
flushInterval | TimeInterval | 15 | Seconds between auto-flushes. |
flushBatchSize | Int | 100 | Events per batch. |
maxQueueSize | Int | 1000 | Drops oldest when exceeded. |
triggerSyncInterval | TimeInterval | 300 | How often the SDK refreshes server-side triggers. |
presentationPaused | Bool | false | Pass true at startup; resume when the host screen is ready. |
Identity & events
UserGist.shared.identify(
userId: "user_42",
properties: ["plan": "pro"],
subjectToken: subjectToken
)
UserGist.shared.track("checkout_completed", properties: [
"orderId": "ord_991",
"amountUsd": 49,
])On logout or an account change, stop producing SDK calls for the old account and wait for local cleanup to succeed before proceeding. This requires 0.1.4 or later:
// Inside your existing async logout/account-switch function:
guard await UserGist.shared.resetAsync() else {
// Keep UserGist calls paused; surface the cleanup failure and offer a retry.
return
}
// Local cleanup succeeded. The next account's UserGist flow may now begin.After success, identify the next account, reapply that account's consent choices, and restore eligible push registration as described in Logout, account switching, and push. resetAsync() waits for local cleanup; remote cleanup is persisted and retried separately. If it returns false, do not identify the next account or resume its events. Callback-based apps can use reset(completion:) and check the same result. Calling plain reset() starts cleanup but does not wait for it.
Consent
Pass the current choices from your app's existing consent flow. In this example, analyticsAllowed, feedbackAllowed, and surveyAllowed are your app's current decisions. pushAllowed is the result of reconciling the saved notification preference with current OS permission using the existing preference migration.
UserGist.shared.setConsent(Consent(
analytics: analyticsAllowed,
feedback: feedbackAllowed,
push: pushAllowed,
survey: surveyAllowed
))Use push: false while push setup is incomplete, when the person opted out, or when OS permission does not allow notifications. Do not leave an unconditional push: false assignment running after restoration on every launch. To change only push consent, call UserGist.shared.setConsent(Consent(push: pushAllowed)); omitted purposes retain their current values.
Surveys
UserGist.shared.getAvailableSurveys { surveys in
guard let id = surveys.first?.id else { return }
DispatchQueue.main.async {
UserGist.shared.openSurvey(id, language: nil)
}
}
UserGist.shared.onResponse = { info in
print("Survey \(info.surveyId) submitted")
}Deep-link survey
func scene(_ scene: UIScene, openURLContexts contexts: Set<UIOpenURLContext>) {
guard let url = contexts.first?.url else { return }
UserGist.shared.handleSurveyDeepLink(url)
}Push notifications
1. Add the Notification Service Extension
In Xcode: File → New → Target → Notification Service Extension. Replace the generated class:
import UserGistFeedback
class NotificationService: UserGistNotificationService {}Add these keys to the extension target's Info.plist:
| Key | Value |
|---|---|
UserGistWriteKey | Your write key |
UserGistApiUrl | https://api.usergist.com |
UserGistAppGroup | A shared App Group ID, e.g. group.com.acme.usergist |
Add the same App Group capability to both the main app and the extension. The SDK uses it to share the anonymous ID between processes.
2. Capabilities
Enable Push Notifications capability on the main app target. For production builds, switch the APNs environment to production in Signing & Capabilities.
3. Request permission and register
Connect this to your existing user-initiated notification flow after SDK initialization. Enable only UserGist push consent after opt-in; preserve other consent choices. For users who already allowed notifications, first apply the existing preference migration: distinguish an absent app preference from an explicit false, preserve opt-outs, and register with APNs without requesting permission again.
// In the app-owned opt-in flow, when permission has not been determined:
UserGist.shared.push.requestPermission { status in
UserGist.shared.setConsent(Consent(push: status == .granted))
// Persist the result using your existing app preference and error handling.
}
// AppDelegate.application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
UserGist.shared.push.didReceiveDeviceToken(deviceToken)4. Forward app activity and notification opens
Call appDidBecomeActive() from your existing app or scene activation callback. This reports app activity for device reachability; it does not record a notification tap.
func applicationDidBecomeActive(_ application: UIApplication) {
UserGist.shared.push.appDidBecomeActive()
}For notification taps and actions, forward the notification response from your existing UNUserNotificationCenterDelegate.userNotificationCenter(_:didReceive:withCompletionHandler:) implementation:
UserGist.shared.push.handleOpened(
userInfo: response.notification.request.content.userInfo,
actionIdentifier: response.actionIdentifier
)Preserve your other notification handlers and call the delegate's completion handler exactly once after handling the response.
Theming
UserGist.shared.setThemeOverrides(PromptTheme(
colors: .init(primary: "#6C5CE7", background: "#FFFFFF"),
radius: 12
))Public surface
| Method | What it does |
|---|---|
initialize(...) | Boots the SDK. |
pausePresentation() / resumePresentation() | Defer and resume campaign UI independently of analytics. |
identify(userId:properties:subjectToken:) | Securely link a stable user ID. |
track(_:properties:) | Queue an event. |
setConsent(_:) | Gate subsystems. |
resetAsync() | Await local identity, consent, and queue cleanup; check the returned boolean before proceeding. |
reset(completion:) | Callback-based local cleanup; check its boolean result. |
flush() | Force flush. |
setDebug(_:) | Toggle logs. |
setThemeOverrides(_:) | Brand the in-SDK UI. |
getAvailableSurveys(completion:) | List targeted surveys. |
openSurvey(_:language:) | Render a survey. |
handleSurveyDeepLink(_:) | Deep-link survey opener. |
onPromptShown / onResponse | Lifecycle callbacks. |
push.requestPermission(...) | Ask for APNs notification permission. |
push.didReceiveDeviceToken(_:) | Register an APNs device token. |
push.appDidBecomeActive() | Report app activity for device reachability. |
push.handleOpened(userInfo:actionIdentifier:) | Forward notification opens, actions, and dismissals. |
push.beaconDelivered(deliveryId:) / push.beaconDisplayed(deliveryId:) / push.beaconDismissed(deliveryId:) | Report observed delivery, display, or dismissal. |