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:

  1. File → Add Packages…
  2. Enter https://github.com/Future-Picnic/usergist-ios.git.
  3. Add the UserGistFeedback product 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

OptionTypeDefaultNotes
writeKeyStringRequired.
environmentEnvironment.production.production, .staging, .development
apiURLURL?nilOverride only for self-hosted.
debugBoolfalseVerbose logging.
flushIntervalTimeInterval15Seconds between auto-flushes.
flushBatchSizeInt100Events per batch.
maxQueueSizeInt1000Drops oldest when exceeded.
triggerSyncIntervalTimeInterval300How often the SDK refreshes server-side triggers.
presentationPausedBoolfalsePass 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.

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")
}
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:

KeyValue
UserGistWriteKeyYour write key
UserGistApiUrlhttps://api.usergist.com
UserGistAppGroupA 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

MethodWhat 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 / onResponseLifecycle 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.

What's next