iOS SDK
Install the TrueEngage iOS SDK, track events and screens, identify users and set up APNs push with a service extension.
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)
In Xcode, add the Push Notifications capability. Add Background Modes → Remote notifications if you want silent pushes.
Upload your APNs key to the iOS provider in the console.
Forward the device token, as in the initialize example above, with
TrueEngage.setAPNsToken(deviceToken).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.
Add the extension target in Xcode and add the TrueEngage package to it.
Turn on the same App Group for the app and the extension.
Set
config.appGroupto that group in your app, as in the initialize example.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.
- Add both hosts from Smart Links, then Domains in the console to your Associated Domains entitlement, as
applinks:<host>entries. - Pass each universal link to the SDK. In UIKit call
TrueEngage.handle(userActivity:). In SwiftUI useonContinueUserActivity(NSUserActivityTypeBrowsingWeb). If you only have a URL, callTrueEngage.handleUniversalLink(url). With UIScene, handle both a warm open inscene(_:continue:)and a cold start inconnectionOptions.userActivities. - Listen with
TrueEngage.onDeepLink { link in ... }and route bylink.deepLinkValueandlink.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
- Run a debug build that uses a
test_key. Open the app and trigger an event. - In the console, switch to Test and go to Audience → Users.
- Search for the id you passed to
identifyand open the profile. - Check the activity list for
App Openedand your own events.

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. |
Was this page helpful?