Skip to main content
Complete guide to implementing push notifications with Zixflow using Apple Push Notifications (APNs) or Firebase Cloud Messaging (FCM).
💡 Track your campaigns: The SDK automatically tracks delivery, opens, and clicks. For advanced tracking customization and implementation details, see Push Notification Tracking.

Prerequisites

Before implementing push notifications:
  1. Add push service credentials to Zixflow dashboard - Configure your APNs certificates or FCM server key in the Zixflow dashboard first
  2. Enable Push Notifications capability in Xcode (see setup steps below)
  3. Install the appropriate push module:
    • ZixflowMessagingPushAPN for Apple Push Notifications
    • ZixflowMessagingPushFCM for Firebase Cloud Messaging

Apple Push Notifications (APNs)

Recommended for most iOS apps. Provides native iOS push notification support.

Step 1: Enable Push Notifications in Xcode

  1. Select your project in Xcode
  2. Go to TargetSigning & Capabilities
  3. Click + Capability → Add Push Notifications
  4. Click + Capability → Add Background Modes
  5. Enable Remote notifications under Background Modes

Step 2: Initialize Zixflow Core SDK

Initialize the core Data Pipelines SDK first in your AppDelegate:
import UIKit
import ZixflowDataPipelines

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Initialize Zixflow SDK
        let config = SDKConfigBuilder(apiKey: "YOUR_API_KEY")

            .autoTrackDeviceAttributes(true)
            .trackApplicationLifecycleEvents(true)
            .logLevel(.debug)
            .build()

        Zixflow.initialize(withConfig: config)

        // Initialize push messaging (see Step 3)

        return true
    }
}

Step 3: Initialize MessagingPushAPN

Add push notification support after initializing the core SDK:
import ZixflowMessagingPushAPN

// In application(didFinishLaunchingWithOptions:)
MessagingPushAPN.initialize(
    withConfig: MessagingPushConfigBuilder()
        .autoFetchDeviceToken(true)        // Auto-request device token
        .autoTrackPushEvents(true)         // Auto-track delivered/opened metrics
        .showPushAppInForeground(true)     // Show notifications when app is active
        .build()
)
Configuration Options
OptionTypeDefaultDescription
autoFetchDeviceTokenBooltrueAutomatically retrieve and register device token
autoTrackPushEventsBooltrueTrack delivered and opened metrics automatically
showPushAppInForegroundBooltrueDisplay push notifications while app is in foreground
appGroupIdString?nilCustom App Group ID for delivery tracking (see Rich Push section)
For automatic push notification handling, use ZixflowAppDelegateWrapper:
import UIKit
import ZixflowDataPipelines
import ZixflowMessagingPushAPN

// Replace @main declaration
@main
class AppDelegateWithZixflowIntegration: ZixflowAppDelegateWrapper<AppDelegate> {}

// Your AppDelegate class (no @main attribute)
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Initialize Zixflow SDK
        let config = SDKConfigBuilder(apiKey: "YOUR_API_KEY")
            
            .build()
        Zixflow.initialize(withConfig: config)

        // Initialize push notifications
        MessagingPushAPN.initialize(
            withConfig: MessagingPushConfigBuilder()
                .autoFetchDeviceToken(true)
                .autoTrackPushEvents(true)
                .showPushAppInForeground(true)
                .build()
        )

        return true
    }
}
Benefits of ZixflowAppDelegateWrapper:
  • Automatically registers device tokens with Zixflow
  • Handles push notification clicks
  • Tracks push metrics (delivered, opened)
  • No additional code required for basic push handling

Step 5: Identify Users

Critical: Device tokens must be associated with a user before push notifications can be delivered.
// Identify user when they log in
Zixflow.shared.identify(
    userId: "user@example.com",
    traits: [
        "email": "user@example.com",
        "first_name": "John",
        "last_name": "Doe"
    ]
)
Important Notes:
  • Device tokens link to user profiles when you call identify()
  • This ensures notifications are delivered to the correct user
  • Always call identify() after user authentication

Step 6: Rich Push with Notification Service Extension

Add support for rich media (images, videos, GIFs) and reliable delivery tracking.
1. Create Notification Service Extension
  1. In Xcode: FileNewTarget
  2. Select Notification Service Extension
  3. Name it (e.g., “NotificationServiceExtension”)
  4. Click Finish (activate scheme when prompted)
2. Add SDK to Extension Target
Swift Package Manager:
  1. Select your project
  2. Go to the Notification Service Extension target
  3. GeneralFrameworks and Libraries
  4. Click + → Add ZixflowMessagingPushAPN
CocoaPods: Update your Podfile:
target 'NotificationServiceExtension' do
  use_frameworks!
  pod 'ZixflowMessagingPushAPN', '~> 1.0'
end
Then run:
pod install
3. Configure Notification Service Extension
Replace NotificationService.swift content:
import UserNotifications
import ZixflowMessagingPushAPN

class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        // Initialize the SDK for the extension
        MessagingPushAPN.initializeForExtension(
            withConfig: MessagingPushConfigBuilder(apiKey: "YOUR_API_KEY")

                .logLevel(.debug)
                .build()
        )

        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        if let bestAttemptContent = bestAttemptContent {
            // Let Zixflow SDK handle rich push and track delivery
            MessagingPushAPN.shared.didReceive(
                request,
                withContentHandler: contentHandler
            )
        }
    }

    override func serviceExtensionTimeWillExpire() {
        // Called when extension is about to be terminated
        MessagingPushAPN.shared.serviceExtensionTimeWillExpire()

        if let contentHandler = contentHandler,
           let bestAttemptContent = bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}
4. Setup App Groups (Recommended)
App Groups enable delivery tracking data sharing between your main app and notification extension. Create App Group:
  1. Select your main app targetSigning & Capabilities
  2. Click + CapabilityApp Groups
  3. Click + to add a new group with format: group.{bundleId}.zixflow
    • Example: group.com.yourcompany.app.zixflow
  4. Repeat for your Notification Service Extension target
Configure in Code:
// In your main app's AppDelegate
MessagingPushAPN.initialize(
    withConfig: MessagingPushConfigBuilder()
        .autoFetchDeviceToken(true)
        .autoTrackPushEvents(true)
        .showPushAppInForeground(true)
        .appGroupId("group.com.yourcompany.app.zixflow")  // Your App Group ID
        .build()
)
Note: If you don’t specify appGroupId, the SDK will automatically infer it from your bundle ID using the pattern group.{bundleId}.zixflow. Only set this if using a custom App Group name.

Firebase Cloud Messaging (FCM)

Use FCM if you need cross-platform push support or already use Firebase services.

Step 1: Setup Firebase Project

  1. Go to https://console.firebase.google.com
  2. Create a new project or select existing project
  3. Add your iOS app to the Firebase project
  4. Download GoogleService-Info.plist
  5. Add GoogleService-Info.plist to your Xcode project root

Step 2: Install Firebase SDK

CocoaPods (recommended for FCM):
# Podfile
platform :ios, '13.0'
use_frameworks!

target 'YourApp' do
  # Zixflow SDK
  pod 'ZixflowDataPipelines', '~> 1.0'
  pod 'ZixflowMessagingPushFCM', '~> 1.0'

  # Firebase
  pod 'FirebaseMessaging', '~> 10.0'
end
Install dependencies:
pod install
Swift Package Manager: Add both Zixflow and Firebase packages to your project.

Step 3: Enable Push Capabilities

Follow the same Xcode capabilities setup as APNs (Step 1 of APNs section).

Step 4: Initialize Firebase and Zixflow

import UIKit
import FirebaseCore
import ZixflowDataPipelines
import ZixflowMessagingPushFCM

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        // Initialize Firebase first (before Zixflow)
        FirebaseApp.configure()

        // Initialize Zixflow SDK
        let config = SDKConfigBuilder(apiKey: "YOUR_API_KEY")
            
            .autoTrackDeviceAttributes(true)
            .trackApplicationLifecycleEvents(true)
            .logLevel(.debug)
            .build()
        Zixflow.initialize(withConfig: config)

        // Initialize FCM messaging
        MessagingPushFCM.initialize(
            withConfig: MessagingPushConfigBuilder()
                .autoFetchDeviceToken(true)
                .autoTrackPushEvents(true)
                .showPushAppInForeground(true)
                .build()
        )

        return true
    }
}

Step 5: Identify Users

Same as APNs - identify users to associate device tokens:
Zixflow.shared.identify(
    userId: "user@example.com",
    traits: ["email": "user@example.com", "name": "John Doe"]
)

Step 6: Rich Push with FCM

Follow the same Notification Service Extension setup as APNs, but import FCM module:
import UserNotifications
import ZixflowMessagingPushFCM

class NotificationService: UNNotificationServiceExtension {
    // ... (same structure as APNs)

    override func didReceive(
        _ request: UNNotificationRequest,
        withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
    ) {
        // Initialize for FCM
        MessagingPushFCM.initializeForExtension(
            withConfig: MessagingPushConfigBuilder(apiKey: "YOUR_API_KEY")
                
                .logLevel(.debug)
                .build()
        )

        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        if let bestAttemptContent = bestAttemptContent {
            MessagingPushFCM.shared.didReceive(request, withContentHandler: contentHandler)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        MessagingPushFCM.shared.serviceExtensionTimeWillExpire()
        // ... rest of implementation
    }
}

Advanced Push Notification Features

Every Zixflow push notification may carry a deeplink_url at the top level and per-button deeplinks in action_buttons. Priority order for navigation:
  1. If an action button was tapped → use action_buttons[index].deeplink
  2. If body was tapped → use deeplink_url
  3. If neither is set → open app to default screen
Handle deep links in notification response:
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    let userInfo = response.notification.request.content.userInfo
    var deeplink: String?

    // Check if action button was tapped
    if response.actionIdentifier != UNNotificationDefaultActionIdentifier &&
       response.actionIdentifier != UNNotificationDismissActionIdentifier {
        let actionIndex = Int(response.actionIdentifier
            .replacingOccurrences(of: "ACTION_", with: "")) ?? -1
        let buttons = parseActionButtons(userInfo["action_buttons"])
        if actionIndex >= 0 && actionIndex < buttons.count {
            deeplink = buttons[actionIndex]["deeplink"] as? String
        }
    }

    // Fall back to main deeplink if no button deeplink
    if deeplink == nil || deeplink?.isEmpty == true {
        deeplink = userInfo["deeplink_url"] as? String
    }

    // Navigate to deeplink
    if let url = deeplink, !url.isEmpty {
        navigateTo(url)
    }

    completionHandler()
}

// Helper to navigate to deeplink
func navigateTo(_ urlString: String) {
    guard let url = URL(string: urlString) else { return }

    // Handle your app's custom URL scheme or universal links
    if url.host == "yourapp.com" {
        switch url.path {
        case "/sale":
            // Navigate to sale screen
            break
        case "/dashboard":
            // Navigate to dashboard
            break
        default:
            // Navigate to home
            break
        }
    } else {
        // External URL - open in Safari
        UIApplication.shared.open(url)
    }
}
Or configure a deep link callback in SDK configuration:
let config = SDKConfigBuilder(apiKey: "YOUR_API_KEY")
    .deepLinkCallback { url in
        print("Deep link: \(url)")
        // Handle navigation
        if url.path == "/product" {
            // Navigate to product screen
            return true  // Return true if handled
        }
        return false  // Return false to open in browser
    }
    .build()

Push Notification Tracking

Zixflow automatically tracks push notification lifecycle events when using ZixflowAppDelegateWrapper. If you need custom tracking or are not using the wrapper, implement tracking manually.
Understanding Push Tracking Events
Zixflow tracks three key push notification events:
  1. Delivered - When the notification arrives on the device
  2. Opened - When the user taps the notification
  3. Action Clicked - When the user taps an action button
The Push Payload
Zixflow injects special fields into every push notification’s userInfo:
FieldPurpose
Zixflow-Delivery-IDUnique ID linking to the campaign delivery
Zixflow-Delivery-TokenThe APNs token the notification was sent to
Example payload:
[
  "Zixflow-Delivery-ID": "626533406292836846",
  "Zixflow-Delivery-Token": "dcFRlDhiRbehM1vg...",
  "title": "Flash Sale! 70% OFF",
  "body": "Limited time only — grab your deal now!",
  "deeplink_url": "https://yourapp.com/sale",
  "action_buttons": "[{\"name\":\"Shop Now\",\"deeplink\":\"https://yourapp.com/sale\"}]"
]
Manual Tracking Implementation
If not using ZixflowAppDelegateWrapper, implement UNUserNotificationCenterDelegate: 1. Track Delivery (Foreground)
import UserNotifications

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        let userInfo      = notification.request.content.userInfo
        let deliveryId    = userInfo["Zixflow-Delivery-ID"] as? String ?? ""
        let deliveryToken = userInfo["Zixflow-Delivery-Token"] as? String ?? ""

        if !deliveryId.isEmpty && !deliveryToken.isEmpty {
            MessagingPushAPN.shared.trackMetric(
                deliveryID:  deliveryId,
                event:       .delivered,
                deviceToken: deliveryToken
            )
        }

        completionHandler([.banner, .sound, .badge])
    }
}
2. Track Opened & Action Clicks
func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
) {
    let userInfo      = response.notification.request.content.userInfo
    let deliveryId    = userInfo["Zixflow-Delivery-ID"] as? String ?? ""
    let deliveryToken = userInfo["Zixflow-Delivery-Token"] as? String ?? ""

    // Always track opened first
    if !deliveryId.isEmpty && !deliveryToken.isEmpty {
        MessagingPushAPN.shared.trackMetric(
            deliveryID:  deliveryId,
            event:       .opened,
            deviceToken: deliveryToken
        )
    }

    // Track action button clicks
    if response.actionIdentifier != UNNotificationDefaultActionIdentifier &&
       response.actionIdentifier != UNNotificationDismissActionIdentifier {

        let actionIndex = Int(response.actionIdentifier
            .replacingOccurrences(of: "ACTION_", with: "")) ?? -1
        let buttons = parseActionButtons(userInfo["action_buttons"])
        let actionName = (actionIndex >= 0 && actionIndex < buttons.count)
            ? buttons[actionIndex]["name"] as? String ?? "Action \(actionIndex + 1)"
            : "Action \(actionIndex + 1)"
        let deeplink = (actionIndex >= 0 && actionIndex < buttons.count)
            ? buttons[actionIndex]["deeplink"] as? String ?? ""
            : ""

        Zixflow.shared.track(
            name: "Push Notification Action Clicked",
            properties: [
                "Zixflow-Delivery-ID":    deliveryId,
                "Zixflow-Delivery-Token": deliveryToken,
                "action_index":           actionIndex,
                "action_name":            actionName,
                "action_deeplink":        deeplink
            ]
        )

        // Navigate to deeplink if provided
        if !deeplink.isEmpty {
            navigateTo(deeplink)
        }
    }

    // Handle main deeplink
    if let deeplink = userInfo["deeplink_url"] as? String, !deeplink.isEmpty {
        navigateTo(deeplink)
    }

    completionHandler()
}

// Helper to parse action buttons JSON
func parseActionButtons(_ raw: Any?) -> [[String: Any]] {
    guard let jsonString = raw as? String,
          let data = jsonString.data(using: .utf8),
          let buttons = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else {
        return []
    }
    return buttons
}
3. Register Delegate
// In didFinishLaunchingWithOptions
UNUserNotificationCenter.current().delegate = self
Note: When using ZixflowAppDelegateWrapper, the SDK handles all tracking automatically. Only implement manual tracking if you need custom behavior or are not using the wrapper.
Decision Flowchart
Use this when deciding which SDK call to make for any notification interaction:
Notification received?
├── YES → trackMetric(.delivered)      [always, as soon as payload arrives]

User interacted?
├── Tapped notification body
│   └── trackMetric(.opened)           [with deliveryID + deviceToken]
│       └── Navigate to deeplink_url

└── Tapped action button
    ├── trackMetric(.opened)           [still track opened first]
    └── track("Push Notification Action Clicked")  [with action_index + action_name]
        └── Navigate to button's deeplink
Key Points:
  • Always track .delivered when the notification arrives
  • Always track .opened before tracking action clicks
  • Action button taps fire both .opened and the action clicked event
  • Use the Zixflow-Delivery-ID and Zixflow-Delivery-Token from the payload for all tracking calls

Manual Device Token Registration

If you disable autoFetchDeviceToken, register manually:
// In AppDelegate
func application(
    _ application: UIApplication,
    didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
) {
    // Register token with Zixflow
    MessagingPushAPN.shared.registerDeviceToken(apnDeviceToken: deviceToken)
}

func application(
    _ application: UIApplication,
    didFailToRegisterForRemoteNotificationsWithError error: Error
) {
    print("Failed to register for remote notifications: \(error)")
    MessagingPushAPN.shared.deleteDeviceToken()
}

Action Buttons

Action buttons allow users to interact with notifications without opening the app. The action_buttons field in the push payload is a JSON-encoded string. Payload format:
"[{\"name\":\"Shop Now\",\"deeplink\":\"https://yourapp.com/sale\"},{\"name\":\"Remind Me\",\"deeplink\":\"\"}]"
Parsed structure:
[
  { "name": "Shop Now",  "deeplink": "https://yourapp.com/sale" },
  { "name": "Remind Me", "deeplink": "" }
]
iOS Requirements: iOS requires action buttons to be pre-registered at app launch:
// In AppDelegate.didFinishLaunchingWithOptions
let actions = [
    UNNotificationAction(identifier: "ACTION_0", title: "Action 1", options: .foreground),
    UNNotificationAction(identifier: "ACTION_1", title: "Action 2", options: .foreground),
]
let category = UNNotificationCategory(
    identifier: "ZX_2BTN",
    actions: actions,
    intentIdentifiers: [],
    options: []
)
UNUserNotificationCenter.current().setNotificationCategories([category])
Rules:
  • Maximum 2 buttons per notification (iOS system limit)
  • Button index is 0-based (leftmost = index 0)
  • deeplink may be empty — handle gracefully
  • Button labels in pre-registration can be generic (“Action 1”, “Action 2”)
  • The actual button names from the payload are tracked in analytics via action_name

Remove Device Token

When user logs out or disables notifications:
// Delete device token from Zixflow
MessagingPushAPN.shared.deleteDeviceToken()

// Also clear user identity
Zixflow.shared.clearIdentify()

Testing Push Notifications

1. Verify Device Token Registration

Check if device token is registered:
if let deviceToken = Zixflow.shared.registeredDeviceToken {
    print("Device token registered: \(deviceToken)")
} else {
    print("Device token not yet registered")
}

2. Enable Debug Logging

Use debug logging to troubleshoot:
let config = SDKConfigBuilder(apiKey: "YOUR_API_KEY")
    .logLevel(.debug)  // Enable verbose logging
    .build()

3. Test on Physical Device

Important: Push notifications require a physical iOS device. The iOS Simulator cannot receive push notifications.

4. Send Test Push from Zixflow

  1. Log into Zixflow dashboard
  2. Navigate to MessagingPush Notifications
  3. Create a test push notification
  4. Send to a specific user (using userId from identify())

Best Practices

  1. Always identify users before sending push notifications
  2. Use Notification Service Extension for rich push and reliable delivery tracking
  3. Configure App Groups for delivery metric persistence
  4. Handle permission requests gracefully - explain value to users
  5. Test thoroughly on physical devices across iOS versions
  6. Enable debug logging during development
  7. Handle deep links to provide seamless user experience
  8. Monitor metrics in Zixflow dashboard to track engagement