iOS SDK

Install the TrueEngage iOS SDK, track events and screens, identify users and set up APNs push with a service extension.

Updated 1 Nov 2026 9 min read

Use the iOS SDK (version 1.0.0) to track events and users in a native iOS app and to send push notifications through the Apple Push Notification service (APNs). It is written in Swift and supports iOS 14 and later.

Before you start

  • An app key for your iOS app. Use a test_ key in debug builds. See Test and Live keys.
  • For push: the Push Notifications capability on your app, and an APNs key uploaded to the iOS provider in the console.

Install

Add the pod to your Podfile, then run pod install:

pod 'TrueEngage', '~> 0.8'

The SDK includes a privacy manifest (PrivacyInfo.xcprivacy).

Initialize

Initialize in application(_:didFinishLaunchingWithOptions:).

import TrueEngage

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    var config = TEConfig(appKey: "test_xxxxxxxx", dataRegion: .india)
    #if DEBUG
    config.logLevel = .verbose
    #endif
    config.appGroup = "group.com.acmeshop.app" // needed for Push Received through the service extension
    TrueEngage.initialize(config: config)
    return true
}

func application(_ application: UIApplication,
                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    TrueEngage.setAPNsToken(deviceToken)
}

In a real app, read the key from your build settings (for example an Info.plist entry) so debug builds use test_ and release builds use live_.

initialize is synchronous and makes no network call. Calls made before it are queued and replayed. TrueEngage.isInitialized tells you if it has run.

Configuration

TEConfig is a struct. Set fields on it before you call initialize.

Field Type Default Meaning
appKey String none test_… or live_…. Selects the environment. Set in the initializer.
apiHost String? region host Full base URL. Set in the initializer. Overrides the region.
dataRegion DataRegion (.india) .india Set in the initializer. Picks the API host. India is the only data region today.
logLevel LogLevel (.none, .error, .warn, .info, .verbose) .error Console logging.
optOutByDefault Bool false Collect nothing until optIn().
flushIntervalSeconds Int 30 Seconds between timed flushes.
flushBatchSize Int 50 Flush when this many items wait.
maxQueueSize Int 10000 Items kept before the oldest are dropped.
sessionTimeoutMinutes Int 30 Idle time that ends a session.
screenTracking Bool false Automatic screen tracking.
flushOnOptOut Bool true On opt-out, send what is queued first. false deletes it.
appGroup String? none App Group shared with the notification service extension.
push.autoRegister Bool true Register for remote notifications on launch and after permission.
push.requestPermissionOnInit Bool false Leave off. Ask at a better moment.
locationPrompt .manual or .auto .manual See Device location.
locationRefreshHours Int 4 Minimum hours between location fixes. Minimum 1.

Identify and log out

TrueEngage.identify("u-1001", attributes: ["firstName": "Priya", "plan": "pro"])
TrueEngage.logout() // ends the session, keeps the push token
TrueEngage.reset()  // full wipe: new anonymous id, queue cleared

Call identify after sign-in with the same id you use in your own systems. Call logout on sign-out. See Events, attributes and identity.

Track events and screens

TrueEngage.track("Order Placed", properties: [
    "orderId": "ORD-8891",
    "amount": 2499.50,
    "itemCount": 3,
    "isFirstOrder": true,
    "placedAt": Date(),
    "categories": ["footwear", "sale"],
])
TrueEngage.track("Sync Finished", properties: ["rows": 120], nonInteractive: true)
TrueEngage.trackScreen("Checkout")

trackScreen sends Screen Viewed and takes optional properties. TrueEngage.flush() sends the queue now. You rarely need it.

User attributes

TrueEngage.setUserAttribute("lifetimeValue", 48200)
TrueEngage.setUserAttributes(["plan": "pro", "city": "Indore"])
TrueEngage.removeUserAttribute("trialEndsAt")

Reserved attributes have their own setters:

TrueEngage.setEmail("priya@example.com")
TrueEngage.setPhone("+919876543210") // E.164, not reformatted
TrueEngage.setFirstName("Priya")
TrueEngage.setLastName("Sharma")
TrueEngage.setGender("female")
TrueEngage.setBirthDate(Date(timeIntervalSince1970: 797_644_800))
TrueEngage.setCity("Indore")
TrueEngage.setRegion("Madhya Pradesh")
TrueEngage.setCountryCode("IN")
TrueEngage.setTimezone("Asia/Kolkata")
TrueEngage.setLocale("en-IN")

Set up push (APNs)

  1. In Xcode, add the Push Notifications capability. Add Background Modes → Remote notifications if you want silent pushes.

  2. Upload your APNs key to the iOS provider in the console.

  3. Forward the device token, as in the initialize example above, with TrueEngage.setAPNsToken(deviceToken).

  4. Ask for permission at a good moment. A user who denies it cannot be asked again by the app.

    TrueEngage.requestPushPermission { result in
        // "granted" | "denied" | "provisional" | "unavailable"
    }
    

With push.autoRegister on, the SDK registers for remote notifications on launch and after permission is granted. The token is sent on every launch with the system notification setting.

The SDK sets itself as the UNUserNotificationCenter delegate and passes calls on to any delegate you set before it. Push Clicked is recorded before your handler runs.

Record background receipts with a notification service extension

Without an extension, iOS gives the app no signal when a push arrives in the background. Add a Notification Service Extension target to get Push Received and rich images.

  1. Add the extension target in Xcode and add the TrueEngage package to it.

  2. Turn on the same App Group for the app and the extension.

  3. Set config.appGroup to that group in your app, as in the initialize example.

  4. Replace the extension's code:

    import TrueEngage
    import UserNotifications
    
    class NotificationService: UNNotificationServiceExtension {
        override func didReceive(_ request: UNNotificationRequest,
                                 withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
            TENotificationServiceHelper.didReceive(request,
                                                   appGroup: "group.com.acmeshop.app",
                                                   contentHandler: contentHandler)
        }
    }
    

The helper posts the receipt, downloads the image if the push has one, and then calls contentHandler. It finishes within 20 seconds.

If you use FCM on iOS

Pass the FCM token instead of the APNs token:

TrueEngage.setPushToken(token, provider: "fcm")

If another delegate replaces the SDK's

Another library, such as firebase_messaging, can replace the notification delegate. Then record taps and receipts yourself:

func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler: @escaping () -> Void) {
    TrueEngage.handleNotificationResponse(response) // Push Clicked or Push Dismissed
    completionHandler()
}

TrueEngage.trackPushClicked(userInfo: userInfo) // a tap you routed yourself
TrueEngage.handlePushPayload(userInfo)          // Push Received; iOS shows the alert itself

Each push is recorded at most once for received and once for clicked, so overlapping calls are safe. Both return false for pushes that do not come from TrueEngage.

To listen natively:

TrueEngage.setEventListener { type, payload in
    // pushClicked, pushReceived, pushDismissed, tokenRegistered, pushPermissionChanged
}

Smart links and deep links

Smart links open your app through universal links. The SDK resolves the link and hands the result to your app, which decides where to go. It never navigates.

  1. Add both hosts from Smart Links, then Domains in the console to your Associated Domains entitlement, as applinks:<host> entries.
  2. Pass each universal link to the SDK. In UIKit call TrueEngage.handle(userActivity:). In SwiftUI use onContinueUserActivity(NSUserActivityTypeBrowsingWeb). If you only have a URL, call TrueEngage.handleUniversalLink(url). With UIScene, handle both a warm open in scene(_:continue:) and a cold start in connectionOptions.userActivities.
  3. Listen with TrueEngage.onDeepLink { link in ... } and route by link.deepLinkValue and link.params.

The listener fires once per open on the main queue. That includes the deferred match after a first install, which needs no code. A link that arrives before you attach a listener is held and delivered when you attach. Never put personal data in link parameters. See Deep links and deferred deep links.

Create links in the app

createLink makes a short smart link for a share, an invite or a referral. Turn on in-app links in Smart Links, then Settings first.

TrueEngage.createLink(TELinkOptions(deepLinkValue: "/product/42", channel: "invite")) { result in
    switch result {
    case .success(let link): share(link.url)
    case .failure(let error): break   // .disabled, .rateLimited, .invalid or .network
    }
}

The completion runs on the main queue, and nothing is retried for you. See Links API and webhooks.

Ad click capture

When a link you pass to handleUniversalLink carries utm_*, gclid, gbraid, wbraid, gad_source, gad_campaignid, fbclid or fbp, the SDK records one touch. A link with none of them records nothing, and nothing is recorded while tracking is opted out. Installs through a smart link are matched by TrueEngage, not by the SDK. Pass your Firebase app instance id so Google Analytics 4 can match app events:

TrueEngage.setFirebaseAppInstanceId(id)

The IDFA is attached only while App Tracking Transparency is authorized. The SDK never shows the prompt. See Advertising id and See where each user came from.

Location

Location is off unless you turn it on. Add NSLocationWhenInUseUsageDescription to your Info.plist, show your own explanation, then ask.

TrueEngage.requestLocationPermission { result in
    // "granted" | "denied" | "restricted" | "not_requested"
}
TrueEngage.updateLocation { stored in /* true if a new fix was stored */ }

Without the Info.plist string, the SDK returns restricted and never asks. See Device location for what is collected and your App Store privacy label.

Consent and opt-out

TrueEngage.optOut()                 // everything; saved across restarts
TrueEngage.optOut(of: "push")       // "tracking" | "push" | "inApp" | "all"
TrueEngage.optIn()
TrueEngage.optIn(to: "push")
TrueEngage.isOptedOut("tracking")

See Consent and opt-out.

Clearing ad identifiers

The SDK tells TrueEngage to delete the stored IDFA when App Tracking Transparency stops being authorised, when the IDFA changes (the clear and the new id travel together), when you pass nil to setAdvertisingIdentifier(_:) or setFirebaseAppInstanceId(_:), and when you call optOut. It happens automatically, only for ids the SDK reported before, and needs no code. An opt-out clear is sent even with flushOnOptOut set to false, and retried while the app is opted out. reset() sends nothing. Requires SDK 0.8.1, which has not yet been compiled or tested on a Mac. See When ad identifiers are cleared.

Debug and validate

let info = TrueEngage.debugInfo() // ids, queue depth, last flush status, counters
TrueEngage.enableValidation()     // logs name and type checks for each event

Set config.logLevel = .verbose in debug builds.

Verify it works

  1. Run a debug build that uses a test_ key. Open the app and trigger an event.
  2. In the console, switch to Test and go to Audience → Users.
  3. Search for the id you passed to identify and open the profile.
  4. Check the activity list for App Opened and your own events.

Priya Banerjee's profile (u-1001) on the Overview tab, with recent sends and Push reachable on Android

Troubleshooting

Symptom Likely cause and fix
No user in the console Wrong environment. A test_ key writes to Test only.
Nothing arrives The device is offline, or the user opted out. Check debugInfo().
Events stop after a while The key was rejected. The SDK pauses for one hour on a bad key.
No push token The Push Notifications capability is missing, or you did not call setAPNsToken. Push does not work in the simulator on every setup, so test on a device.
Push arrives but no Push Received Add the notification service extension and set appGroup in both places. Foreground receipts work without it.
Taps are not recorded Another library replaced the notification delegate. Call handleNotificationResponse.
Location returns restricted NSLocationWhenInUseUsageDescription is missing from Info.plist.