iOS Integration
This guide shows how to add the AlgorithmX SDK to an iOS app that is already in production, with its own login, analytics, push notifications, and navigation. You do not rebuild any of these. You add a few calls in the places where your app already handles startup, sign-in, business events, notifications, and navigation. The SDK does the rest: it shows in-app campaigns, routes campaign notifications, tracks interactions, and applies frequency rules.
The SDK module is EngageSDK, and every call goes through AlgorithmX.shared. Names such as AppRouter and AlgorithmXRouting in the examples stand for your own code; they are not SDK APIs.
How the SDK fits into your app
| Where in your existing app | What you add | Section |
|---|---|---|
| Xcode project | The EngageSDK package. | 2 |
| AppDelegate startup | Initialize the SDK and register your handlers. | 3 |
| Login, restored session, logout | Tell the SDK who the customer is. | 4 |
| Business events (product viewed, order confirmed, and so on) | Forward the agreed events. | 5 |
| APNs token callback and notification delegate | Pass the token and AlgorithmX notifications to the SDK. | 6 |
| Deep-link router | Let campaign links open your existing screens. | 7 |
| Business actions and screens | Handle campaign custom actions and notification buttons. | 8 |
| Notification service extension (optional) | One call for images, buttons, and delivery reporting. | 10 |
In-app campaigns need no screen or view code from you: once these connections are in place, the SDK shows campaigns by itself (section 9).
1. Before you start
What AlgorithmX gives you
- The SDK package and its version, as a Swift package or a CocoaPods podspec.
- The API base URL for each environment, such as staging and production. All URLs use HTTPS.
What your team gives AlgorithmX
AlgorithmX sends iOS notifications to your app through Apple's push service (APNs) on your behalf, so it needs a few details from your Apple Developer account:
| Item | Example | Where to find it |
|---|---|---|
| Bundle ID of each app build | com.example.shop | Xcode, target settings. |
| Team ID | 10 characters, such as ABCDE12345 | Apple Developer account, Membership details. |
| APNs authentication key and its Key ID | AuthKey_ABC123DEFG.p8 | Apple Developer account, Keys (see the checklist below). |
| Which build type each AlgorithmX workspace serves | Staging: debug builds. Production: TestFlight and App Store builds. | Your release process. Debug builds and store builds use different APNs environments, so a workspace must match the builds it sends to. |
Also agree with the AlgorithmX team on the customer ID you will use (section 4), the events you will send (section 5), and the deep links and custom actions campaigns may use (sections 7 and 8).
Check your Apple Developer account
An app that already receives push notifications usually has everything except the key for AlgorithmX.
- Push Notifications is enabled for the app. Existing push apps already have it.
- An APNs authentication key. Reuse your team's existing key if your security policy allows, or create one under Certificates, Identifiers & Profiles, then Keys, with Apple Push Notifications service enabled. Apple lets you download the
.p8file only once. Send it to AlgorithmX through the agreed secure channel, and never include it in the app. See token-based connection to APNs. - Background Modes, Remote notifications is enabled on the app target in Xcode. In-app campaigns arrive as background messages and need it.
- App Group (optional): only if you want the notification extension to report delivery (section 10).
2. Add the SDK
The SDK needs iOS 15 or later and Xcode 15 or later, and has no third-party dependencies.
Swift Package Manager: in Xcode, choose File, Add Package Dependencies, enter the package URL from AlgorithmX (or choose Add Local for a delivered folder), and add the EngageSDK product to your app target. If you use section 10, add it to your notification service extension target too.
CocoaPods: add the pod to your app target. The path is an example; use the one AlgorithmX gives you.
target 'YourApp' do
pod 'EngageSDK', :path => '../algorithmx-ios-sdk'
endWith CocoaPods, do not add the pod to the notification service extension; section 10 explains what to add there instead.
3. Initialize the SDK in your AppDelegate
Add these lines to your existing application(_:didFinishLaunchingWithOptions:). Keep your current startup code as it is.
import EngageSDK
import UIKit
import UserNotifications
final class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
// The SDK holds its handlers weakly: keep a strong reference for the app's lifetime.
private let algorithmXRouting = AlgorithmXRouting()
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
// Keep your existing startup code.
AlgorithmX.shared.initialize(apiBaseUrl: "https://api.example.com", appGroup: "group.com.example.shop")
#if DEBUG
AlgorithmX.shared.setLoggingEnabled(true)
#endif
// Application-owned adapter implemented in sections 7 and 8.
AlgorithmX.shared.notificationClickListener = algorithmXRouting
AlgorithmX.shared.deepLinkHandler = algorithmXRouting
AlgorithmX.shared.actionButtonHandler = algorithmXRouting
UNUserNotificationCenter.current().delegate = self // Keep your existing delegate assignment.
application.registerForRemoteNotifications() // Every launch, independent of permission.
return true
}
}- Initialize here, not on the first screen. iOS also starts your app in the background to deliver campaign messages; no screen appears then, but this method still runs. Calls made before
initializeare ignored. - Keep your handlers alive. The SDK holds
notificationClickListener,deepLinkHandler, andactionButtonHandlerweakly. Store the object in the AppDelegate, as above. An object that nothing else keeps, such asAlgorithmX.shared.deepLinkHandler = MyHandler(), disappears immediately and campaign taps then do nothing. - App Group is optional. Without it, call
AlgorithmX.shared.initialize(apiBaseUrl:). - SwiftUI apps keep their
Appstructure and connect the AppDelegate with the adaptor they may already have:
@main
struct ShopApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene { WindowGroup { RootView() } }
}4. Identify your customers
Until you identify a customer, the SDK uses an anonymous device ID. Call identifyUser with the same customer ID your backend uses, in two places: after a successful login, and when your app restores a saved session at launch. The second one matters for customers who are already signed in when they update to the first release with AlgorithmX.
// In the existing restored-session or successful-login hook.
AlgorithmX.shared.identifyUser(
userId: "customer_123",
attributes: ["first_name": "Amina", "language": "ar", "loyalty_tier": "gold"]
)The SDK remembers the customer across app restarts and sends their previous anonymous ID, so AlgorithmX can link earlier activity to the profile. At logout, and when one account replaces another, clear waiting campaigns so the next person does not see them:
// Before completing logout:
_ = AlgorithmX.shared.clearWebViewQueue()
AlgorithmX.shared.resetIdentity()
// Before replacing one signed-in account with another:
_ = AlgorithmX.shared.clearWebViewQueue()
AlgorithmX.shared.identifyUser(userId: "customer_456", attributes: ["language": "en"])resetIdentity()returns to the anonymous device ID. It does not close a campaign that is already on screen; if your logout flow closes open screens, include the campaign screen (CampaignViewController).- Attribute values follow the same type rule as event properties in section 5.
- AlgorithmX identity follows your login; it does not replace your authentication.
5. Track business events
Forward the agreed events from the places where your app already knows an action succeeded, for example your order-confirmation handler, next to your existing analytics calls.
AlgorithmX.shared.trackEvent("product_view", properties: ["product_id": "SKU-123"])
AlgorithmX.shared.trackEvent("add_to_cart", properties: ["product_id": "SKU-123", "quantity": 2])
AlgorithmX.shared.trackEvent("purchase", properties: [
"order_id": "ORDER-1001",
"total": 149.90,
"currency": "SAR",
"items": [["product_id": "SKU-123", "quantity": 2]]
])- Use only JSON value types:
String,Int,Double,Bool, and arrays or dictionaries of them. Any other value, such as aDate,URL, custom type, orDouble.nan, crashes the app. Convert dates to a timestamp or an ISO 8601 string, and leave out empty optional values. - Use simple event names such as
add_to_cart; the name becomes part of the request URL. - Events are not retried. An event sent while the device is offline is lost, so keep your own backend as the source of truth for orders and payments.
- No history is imported. Past activity reaches AlgorithmX only through a separate data import agreed with the AlgorithmX team.
6. Connect your existing push notifications
Your app keeps its current push setup. You add two things: pass the device token to AlgorithmX, and pass AlgorithmX notifications to the SDK.
Pass the device token
Add one line to your existing token callback:
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
let apnsToken = deviceToken.map { String(format: "%02x", $0) }.joined()
AlgorithmX.shared.registerDeviceToken(apnsToken)
// Keep your existing registrations here, for example Firebase's apnsToken assignment.
}- Send the APNs token, even if your app also uses Firebase Messaging; the Firebase registration token does not work with AlgorithmX.
- Register it on every launch, which is why section 3 calls
registerForRemoteNotifications()each time. Customers who update your app already have a token that AlgorithmX has never seen. If your code skips registration when the token is unchanged, do not skip the AlgorithmX call. - Token registration does not need the customer's notification permission. Keep your existing permission prompt; the SDK never asks for permission.
Forward AlgorithmX notifications from your delegate
Your app has one notification delegate. Add an AlgorithmX branch at the start of the three methods below; isAlgorithmXPush picks out AlgorithmX notifications, and everything else continues through your current code.
// Silent (background) pushes. Called in the foreground and in the background.
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
if AlgorithmX.shared.isAlgorithmXPush(userInfo: userInfo) {
AlgorithmX.shared.handleNotification(userInfo: userInfo) {
completionHandler(.newData)
}
return
}
// Keep your current handling here; it must call completionHandler exactly once.
}
// A visible notification arrives while the app is in the foreground.
func userNotificationCenter(
_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
let userInfo = notification.request.content.userInfo
if AlgorithmX.shared.isAlgorithmXPush(userInfo: userInfo) {
AlgorithmX.shared.handleNotification(userInfo: userInfo) {
completionHandler([.banner, .list, .sound]) // Your foreground presentation policy.
}
return
}
// Keep your current foreground presentation here.
}
// The user taps a notification or one of its buttons.
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
let userInfo = response.notification.request.content.userInfo
if AlgorithmX.shared.isAlgorithmXPush(userInfo: userInfo) {
AlgorithmX.shared.handleNotificationResponse(
actionIdentifier: response.actionIdentifier,
userInfo: userInfo,
completionHandler: completionHandler
)
return
}
// Keep your current tap handling here.
}- The first method receives in-app campaign messages. The second lets you decide whether AlgorithmX notifications show a banner while the app is open. The third routes taps and buttons to your handlers from sections 7 and 8, and records opens and clicks.
- The SDK calls the completion handler you pass it, so each one is still called exactly once.
- Keep setting the delegate inside
didFinishLaunchingWithOptions. If it is set later, a tap that launches the app is lost. - If another SDK, such as Firebase, takes over your delegate, send a real AlgorithmX notification during testing to check that these branches still run.
- Taps that launch the app: the SDK calls your handlers immediately after launch, which can be before your first screen or restored session is ready. Your router must accept the destination and open it once the UI is ready, for example by keeping a pending route.
7. Connect deep links to your router
When a campaign's action is "open screen", the SDK passes the campaign's link to your DeepLinkHandler. Connect it to the router your app already uses for links:
final class AlgorithmXRouting: NSObject, NotificationClickListener, DeepLinkHandler, ActionButtonHandler {
func onDeepLinkReceived(url: URL) -> Bool {
AppRouter.shared.open(url)
}
// NotificationClickListener and ActionButtonHandler methods: section 8.
}- Your router does the navigation. The SDK never opens the link itself; if no handler is set, nothing happens.
- Return true when your router accepts the link, including when it waits for login first, and false when it rejects it.
- Reuse your existing links. Because the SDK hands the link straight to your router, you do not need a new URL scheme, universal-link setup, or new screens. Give the campaign team the list of links your app supports, with their required parameters, for example
myapp://open/product?id=SKU-123orhttps://shop.example.com/cart. - Links opened from outside the app keep working through your existing
onOpenURLor SceneDelegate code; call the same router from both places.
8. Handle custom actions and notification buttons
Each campaign notification has one main action, chosen by the campaign team:
| Campaign action | What happens | Your code |
|---|---|---|
| Open screen | The link goes to your DeepLinkHandler. | Section 7. |
| Custom action | The SDK calls onCustomAction with the action name and its parameters. | Map each agreed action name to your screen or flow. |
| Open web page | The SDK opens the URL in Safari. | None. |
| Open in-app campaign | The SDK shows the campaign page over your app. | None. |
Add the custom actions to the same adapter object:
// Inside AlgorithmXRouting from section 7.
func onNotificationClick(data: [String: Any]) -> Bool {
false // Let the SDK run the default route for action_type.
}
func onCustomAction(action: String?, data: [String: Any]) -> Bool {
let parsed = data["parsedActionData"] as? [String: Any] ?? [:]
switch action {
case "view_product":
guard let productId = parsed["product_id"] as? String, !productId.isEmpty else { return false }
return AppRouter.shared.openProduct(id: productId)
case "view_cart":
return AppRouter.shared.openCart()
default:
return false
}
}onNotificationClickruns first for every tap. Return false to let the SDK run the campaign's action; return true only if your own code fully handled the tap.- The action's parameters are in
parsedActionData. Agree on the action names and parameter types with the campaign team: a numeric value arrives as a number, soas? Stringfails for it. - An action name your app does not know does nothing, so only offer campaign actions that your released app version implements.
Notification buttons have their own handler. Match on the button's action text, not on its translated title:
// Inside AlgorithmXRouting from section 7.
func onActionButtonClicked(
buttonId: String,
actionText: String,
title: String,
notificationData: [String: Any]
) -> Bool {
switch actionText {
case "view_cart":
return AppRouter.shared.openCart()
case "dismiss":
return true // Prevent the notification's main-action fallback.
default:
return false
}
}Every button opens the app. Return true when you handled the button. If you return false, the SDK runs the notification's main action instead. Buttons appear only if you set up section 10.
A campaign parameter is only a navigation hint: keep your normal login and permission checks before sensitive actions.
9. In-app campaigns
In-app campaigns need no extra code. With sections 3 to 6 in place:
- AlgorithmX sends a background message to the device, and your delegate passes it to the SDK.
- The SDK saves the campaign.
- The next time the customer opens or returns to the app, the SDK shows the campaign over the current screen, at most one per visit, and closes it when the customer taps its close button.
Points to check in your app:
- Background Modes, Remote notifications must be enabled (section 1). Without it, campaigns only arrive while the app is open.
- Logout and account switch: clear waiting campaigns as shown in section 4.
- Your own pop-ups at launch: a campaign can appear at the moment the app opens, on top of whatever is on screen. If your app shows its own pop-up at that moment, check
AlgorithmX.shared.isWebViewCurrentlyShowing()first or present it from the top-most screen. - Close button: iOS has no Back button, so every campaign template must include a close button. Remind your campaign team.
- Delivery is best effort. Apple may delay background messages, and does not deliver them to an app the customer force-closed from the app switcher until it is opened again.
To follow campaign activity while testing, you can set AlgorithmX.shared.campaignInteractionListener to an object you keep a reference to; it reports each impression, click, and close.
10. Optional: images, buttons, and delivery reporting
iOS displays notifications itself. To show AlgorithmX images and buttons, and to report that a notification was delivered, add one call to your notification service extension. Without it, AlgorithmX notifications still appear and taps still work, but without images or buttons.
If your app already has a notification service extension, add the AlgorithmX branch to it; iOS uses one extension per app. Otherwise, add one in Xcode with File, New, Target, Notification Service Extension, set its deployment target to the same version as your app, and link EngageSDK to it.
import EngageSDK // Remove this line when the helper file is compiled into the extension (CocoaPods).
import UserNotifications
final class NotificationService: UNNotificationServiceExtension {
private var contentHandler: ((UNNotificationContent) -> Void)?
private var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
guard let content = request.content.mutableCopy() as? UNMutableNotificationContent else {
contentHandler(request.content)
return
}
bestAttemptContent = content
if EngageNotificationService.isAlgorithmXPush(request) {
EngageNotificationService.processNotification(
request: request,
bestAttemptContent: content,
appGroup: "group.com.example.shop", // The same group passed to initialize.
contentHandler: contentHandler
)
return
}
// Keep your current processing for other notifications here.
contentHandler(content)
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler, let bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}- Delivery reporting needs an App Group shared by the app and the extension, with the same identifier in
initialize(section 3) and here. Without an App Group, callprocessNotification(request:bestAttemptContent:contentHandler:); images and buttons still work. - CocoaPods: instead of linking the pod, add the SDK file
Sources/EngageSDK/Notifications/EngageNotificationService.swiftto the extension target only, and remove theimport EngageSDKline.
11. Test and ship
Test on a physical iPhone with a test customer, and repeat the main checks with a TestFlight build, because store builds use a different push environment from debug builds.
| Check | Expected result |
|---|---|
| Update from your current App Store version while signed in | The customer is identified without logging in again, and the token is registered. |
| Login, restart, logout, second account | Events use the right customer each time, and no campaign from the first account appears for the second. |
| Your existing notifications | Other notifications behave exactly as before. |
| AlgorithmX notification with the app open, in the background, and closed | It appears according to your foreground policy, and a tap opens the right screen once, including when the tap launches the app. |
| Each deep link and custom action your app supports | The right screen opens; a missing parameter or unknown link follows your app's normal fallback. |
| Each notification button | The button's action runs, or the main action when your handler returns false. |
| In-app campaign | It appears the next time the app becomes active, over the current screen, and closes with its close button. |
| Notification permission denied | In-app campaigns still arrive; visible notifications do not. |
| Business events | The agreed events appear for the customer in AlgorithmX. |
While testing, setLoggingEnabled(true) prints each SDK request to the Xcode console. On a device without the debugger, run xcrun devicectl device process launch --console --terminate-existing --device <device-id> <bundle-id> to see the same output.
App Store privacy details
- The SDK includes a privacy manifest (no tracking; device ID, user ID, and product interactions), which Xcode adds to your app's privacy report.
- Your App Store privacy answers must also cover what you send yourself through
identifyUserattributes andtrackEventproperties, such as name, email, or purchases. - The SDK uses the vendor identifier, not the advertising identifier, and does not show the App Tracking Transparency prompt.
API reference
| Call on AlgorithmX.shared | Use |
|---|---|
initialize(apiBaseUrl:) / initialize(apiBaseUrl:appGroup:) | Start the SDK in didFinishLaunchingWithOptions. |
setLoggingEnabled(_:) | Print SDK requests to the console while testing. Off by default. |
identifyUser(userId:attributes:) | Identify the customer after login and when a session is restored. |
resetIdentity() | Return to the anonymous device ID at logout. |
trackEvent(_:properties:) | Send a business event. |
registerDeviceToken(_:) | Pass the APNs token on every launch. |
isAlgorithmXPush(userInfo:) | Check whether a notification comes from AlgorithmX. |
handleNotification(userInfo:completionHandler:) | Forward from didReceiveRemoteNotification and willPresent. |
handleNotificationResponse(actionIdentifier:userInfo:completionHandler:) | Forward from didReceive response. |
deepLinkHandler, notificationClickListener, actionButtonHandler, campaignInteractionListener | Your handlers. Keep a strong reference to each; set nil to remove. |
clearWebViewQueue() | Remove waiting campaigns at logout or account switch. |
isWebViewCurrentlyShowing() | Check whether a campaign is on screen. |
getDeviceFingerprint(), getQueueSize() | Inspect the current customer ID and waiting campaigns while testing. |
EngageNotificationService.processNotification(...) | Call from your notification service extension (section 10). |
Troubleshooting
| Symptom | What to check |
|---|---|
| No events in AlgorithmX | initialize runs in didFinishLaunchingWithOptions with the right HTTPS URL, and the device is online. |
| App crashes when sending an event | A property value is not a JSON type, for example a Date. Convert it first. |
| No AlgorithmX notifications arrive | The token is registered with AlgorithmX on every launch; AlgorithmX has your key, Team ID, and bundle ID; the workspace matches the build type (debug or TestFlight/App Store). |
| Tapping a notification does nothing | Your handlers are still referenced by the AppDelegate, the delegate is set in didFinishLaunchingWithOptions, and the AlgorithmX branch in didReceive response runs. |
| Deep link does nothing | DeepLinkHandler is set and calls your router; the link is one your router accepts; on launch from a tap, your router waits for the UI. |
| A button opens an unexpected screen | Your button handler returned false, so the main action ran. Check the action text. |
| No images or buttons | Your notification service extension calls processNotification and has the SDK linked (or the helper file with CocoaPods). |
| In-app campaign never appears | Background Modes with Remote notifications is on, didReceiveRemoteNotification forwards to the SDK, the app was not force-closed, and you reopened the app after the campaign was sent. |
| Campaign cannot be closed | The campaign template needs a close button; ask your campaign team. |
| A previous account's campaign appears | Call clearWebViewQueue before logout and account switch. |
For creating campaigns, continue with In-App Push Campaigns and In-App View Campaigns. Return to Integrations for the other platforms.
