Skip to main content

iOS - Push Lifecycle Module

note

To receive push notification registration and interaction events, a proper implementation of the PushLifecycleModule protocol is required. Q2MobileCore will interact with your module for various push notification events.

The PushLifecycleModule interface is used when creating a module that needs to respond to push notifications. Q2MobileCore interacts with the module in the following cases:

  • When asking for user notification categories that need to be registered by the application
  • When delivering user notification registration events, example: device token success or failure
  • When delivering silent user notification
  • When push notification was tapped or one of its buttons
  • When push notification received while the application is in the foreground

Quick Start Guide

Follow these steps to implement push notification support in your module:

Step 1: Create Your Module Class

import Q2ModuleInterface

class YourPushLifecycleModule: PushLifecycleModule {

var moduleDelegate: ModuleDelegate?
var moduleDataSource: ModuleDataSource?

// Required for explicit module targeting (25.7.0+)
public static var sender: PushSender? {
return .moduleSender
}

// Handle notification categories
public func getNotificationCategories() -> Set<UNNotificationCategory>? {
return nil // Add your custom categories here
}

// Handle device token registration
public func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let token = String(deviceToken: deviceToken)
// Send token to your backend
print("Device token: \(token)")
}

// Handle registration failures
public func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("Failed to register for notifications: \(error)")
}

// Handle silent notifications
public func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult? {
// Process silent notification
return .newData
}

// Handle notification taps
public func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async {
let userInfo = response.notification.request.content.userInfo
// Handle the notification tap
}

// Handle foreground notifications
public func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions? {
return [.banner, .sound, .badge]
}
}

extension PushSender {
static var moduleSender: Self {
PushSender(id: "com.q2.mobile.module.{your-unique-identifier}")
}
}

private extension String {
init(deviceToken: Data) {
self = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
}
}

Step 2: Configure Module Settings

Update your settings.json to enable push notifications:

{
"name": "your-module-name",
"packageName": "your-package-name",
"productName": "YourProduct",
"className": "YourPushLifecycleModule",
"identifier": "com.q2.mobile.module.your-identifier",
"data": "",
"enabled": true,
"push": true
}

Step 3: Test Your Implementation

  1. Enable push notifications in DevApp:

    • Launch DevApp on device or Apple Silicon simulator
    • Sign in and complete device setup
    • Allow push notifications when prompted
  2. Send test notification:

    {
    "aps": {
    "alert": {
    "title": "Test Notification",
    "body": "Your module is working!"
    }
    }
    }
  3. Verify module receives the notification in your implementation methods

Explicit Module Targeting (25.7.0+)

New Feature

Q2MobileApp now supports explicit module targeting for push notifications, enabling multiple modules to independently process and render rich content notifications. This is the recommended approach for new implementations.

Overview

Each module must expose a unique identifier to ensure proper notification routing. When multiple modules exist, the system uses the sender field in the notification payload to determine which module should handle the notification.

Setup PushSender Identifier

Create a unique identifier for your module:

extension PushSender {
static var moduleSender: Self {
PushSender(id: "com.q2.mobile.module.{your-unique-identifier}")
}
}

Implementation Requirements

For PushLifecycleModule implementations:

class CustomPushLifecycleModule: PushLifecycleModule {
public static var sender: PushSender? {
return .moduleSender
}
// ... other implementation
}

For Q2ModuleBase subclasses:

public class YourModuleClass: Q2ModuleBase {
public override static var sender: PushSender? {
return .moduleSender
}
// ... other implementation
}

Notification Payload Format

Include the sender key in your notification payload:

{
"aps": {
"alert": {
"title": "Your Title",
"subtitle": "Your Subtitle",
"body": "Your Body"
},
"mutable-content": 1
},
"sender": "com.q2.mobile.module.{your-unique-identifier}"
}

Runtime Resolution

  • MobileCore reads the sender value from the payload
  • If a matching module is found, only that module handles the notification
  • If no match is found or key is missing, fallback to legacy behavior

Protocol Reference

PushLifecycleModule Protocol

All methods in the PushLifecycleModule protocol are optional, allowing you to implement only the functionality your module needs.

public protocol PushLifecycleModule: LifecycleModule, PushSenderProvider {

// MARK: - Push Sender (Required for 25.7.0+)
static var sender: PushSender? { get }

// MARK: - Notification Categories
func getNotificationCategories() -> Set<UNNotificationCategory>?

// MARK: - Registration Events
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)

func application(_ application: UIApplication,
didFailToRegisterForRemoteNotificationsWithError error: Error)

// MARK: - Notification Handling
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult?

func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse) async

func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification) async -> UNNotificationPresentationOptions?
}

Method Details

static var sender: PushSender?

  • Required for 25.7.0+: Provides unique module identifier for explicit targeting
  • Return: PushSender instance with your module's unique identifier
  • Usage: Used by MobileCore to route notifications to correct module

getNotificationCategories() -> Set<UNNotificationCategory>?

  • Purpose: Register custom notification categories with interactive actions
  • Return: Set of UNNotificationCategory objects, or nil if no categories needed
  • Called: During app launch when registering for notifications

application(_:didRegisterForRemoteNotificationsWithDeviceToken:)

  • Purpose: Handle successful device token registration
  • Parameters: Application instance and device token data
  • Usage: Send device token to your backend service

application(_:didFailToRegisterForRemoteNotificationsWithError:)

  • Purpose: Handle device token registration failures
  • Parameters: Application instance and error details
  • Usage: Log error and implement fallback behavior

application(_:didReceiveRemoteNotification:) async -> UIBackgroundFetchResult?

  • Purpose: Handle silent push notifications (background app refresh)
  • Parameters: Application instance and notification payload
  • Return: UIBackgroundFetchResult indicating processing result
  • Usage: Process data updates without user interaction

userNotificationCenter(_:didReceive:) async

  • Purpose: Handle notification interactions (tap, action button press)
  • Parameters: Notification center and response containing action details
  • Usage: Navigate to specific screens, perform actions based on user interaction

userNotificationCenter(_:willPresent:) async -> UNNotificationPresentationOptions?

  • Purpose: Configure how notification appears when app is in foreground
  • Parameters: Notification center and notification object
  • Return: Presentation options (banner, sound, badge) or nil for default behavior
  • Usage: Customize notification display for foreground scenarios

Extension Protocols

PushServiceExtensionModule

public protocol PushServiceExtensionModule: Module, UNNotificationServiceExtension, PushSenderProvider {
static var sender: PushSender? { get }
// Inherits UNNotificationServiceExtension methods
}

PushContentExtensionModule

public protocol PushContentExtensionModule: Module, UIViewController, UNNotificationContentExtension, PushSenderProvider {
static var sender: PushSender? { get }
// Inherits UNNotificationContentExtension methods
}

Implementation

Implementing a PushLifecycleModule is straightforward—create an NSObject class conforming to the PushLifecycleModule protocol requirements.

Sample PushLifecycleModule Implementation
import Q2ModuleInterface

class CustomPushLifecycleModule: PushLifecycleModule {

var moduleDelegate: ModuleDelegate?

var moduleDataSource: ModuleDataSource?

public static var sender: PushSender? {
return .moduleSender
}

public func getNotificationCategories() -> Set<UNNotificationCategory>? {
return nil // or provide set of user notification categories here.
}

public func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
// provide device token to your API / SDK if required.
let token = String(deviceToken: deviceToken)
print(token)
}

public func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
// inform your API / SDK device registration failed.
}

public func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) async -> UIBackgroundFetchResult? {
// silent notification
return nil // or valid instance of UIBackgroundFetchResult
}

public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async {
// handle notification when user taps on it.
}

public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -> UNNotificationPresentationOptions? {
return nil // or valid instance of UNNotificationPresentationOptions
}
}

private extension String {

init(deviceToken: Data) {
self = deviceToken.map { String(format: "%.2hhx", $0) }.joined()
}
}

Enable Push Notification Permission Popup

Pre-requisites

  1. Update settings.json as shown below:
{
"push": true,
"modules": [
{
"name": "Push Notifications",
"packageName": "Q2MobileCore",
"className": "PushEnrollmentModule",
"identifier": "pushNotification",
"data": "",
"enabled": true,
"isPublic": false
},
{
"name": "Q2Firebase (internal)",
"packageName": "Q2Firebase",
"productName": "Q2Firebase",
"className": "Q2FirebaseModule",
"data": {
"googleAppId": "{GoogleService-Info.plist-googleAppId}",
"gcmSenderId": "{GoogleService-Info.plist-gcmSenderId}",
"apiKey": "{GoogleService-Info.plist-apiKey}",
"projectId": "{GoogleService-Info.plist-projectId}"
},
"enabled": true,
"isPublic": false,
"isInternal": true
}
]
}
note
  • Go to Q2DevAppDependencies/Package.swift
  • Add few characters at the end of the file like //update
  • This will trigger re-download of required packages and you will see firebase and other dependencies pulled in under packages.

Trigger Push Notification Permission

  1. Launch Q2DevApp on Development device or on simulator if running on Apple Silicon Macs.
  2. Perform SignIn.
  3. Observe Device Setup screen with Allow Push Notifications step.
  4. Tap Allow Notifications.
  5. Grab device token logged from didRegisterForRemoteNotificationsWithDeviceToken method.

Send Notification using Apple Cloud Console

We will use Apple Push Notification option within Apple's Cloud Console to send various types of push notification on simulator if you are using Apple Silicon Macs or on actual device.

  1. Go to Apple Developer Cloud Kit Console (https://icloud.developer.apple.com/dashboard/)
  2. Select Appropriate Team.
  3. Select Push Notifications.
  4. Select Correct Bundle Identifier from left drop down. Make sure its same bundle id configured in DevApp as well as in firebase (if you are testing using firebase)
  5. Create new Push Notification.
  6. Give Name to notification.
  7. Keep Environment to Development.
  8. Enter Device Token which you copied from Xcode Console.
  9. Set apns-priority to HIGH.
  10. Hit Send.
  11. Observe notification delivered to device/simulator and content of notification is changed.

Rich Push Notification - Service Extension

The service extension module intercepts push payloads from apps, and gives you the chance to change content in the notification before it is presented. Read more on: https://developer.apple.com/documentation/usernotifications/modifying-content-in-newly-delivered-notifications

Implementation

Create a class inherited from UNNotificationServiceExtension and conforming to PushServiceExtensionModule as shown below:


public class DemoNotificationServiceExtensionModule: UNNotificationServiceExtension, PushServiceExtensionModule {

public var moduleDelegate: ModuleDelegate!
public var moduleDataSource: ModuleDataSource!

public static var sender: PushSender? {
return .moduleSender
}

public override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {

// Store the content handler for timeout scenarios
self.contentHandler = contentHandler

guard let content = request.content.mutableCopy() as? UNMutableNotificationContent else {
print("Failed to create mutable notification content")
contentHandler(request.content)
return
}

// Check if this notification is intended for our module
guard let senderID = content.userInfo["sender"] as? String,
senderID == "com.q2.mobile.module.{your-unique-identifier}" else {
print("Notification not intended for this module")
contentHandler(request.content)
return
}

// Parse custom payload
guard let payload = DemoNotificationPayload.make(fromNotificationContent: content) else {
print("Failed to parse custom payload, using original content")
contentHandler(request.content)
return
}

// Process payload and update content
content.title = payload.title
content.subtitle = payload.subtitle
content.body = payload.body

// Add custom attachment if URL is provided
if let imageURL = payload.imageURL {
downloadAndAttachImage(from: imageURL, to: content) { [weak self] in
self?.contentHandler?(content)
}
} else {
contentHandler(content)
}
}

// Handle timeout scenarios
public override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler {
contentHandler(request.content)
}
}

// Helper method for downloading images
private func downloadAndAttachImage(from url: URL, to content: UNMutableNotificationContent, completion: @escaping () -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
defer { completion() }

guard let data = data, error == nil else {
print("Failed to download image: \(error?.localizedDescription ?? "Unknown error")")
return
}

do {
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
.appendingPathComponent("notification-image")
.appendingPathExtension("jpg")

try data.write(to: tempURL)

let attachment = try UNNotificationAttachment(identifier: "image", url: tempURL, options: nil)
content.attachments = [attachment]
} catch {
print("Failed to create attachment: \(error.localizedDescription)")
}
}.resume()
}

// Store references for timeout handling
private var contentHandler: ((UNNotificationContent) -> Void)?
private var request: UNNotificationRequest!
}

// MARK: - DemoNotificationPayload

extension DemoNotificationServiceExtensionModule {

struct DemoNotificationPayload: NotificationPayloadConvertible {

static var key: String = "demo" // use key which represents your payload.

let title: String
let subtitle: String
let body: String
let imageURL: URL?

enum CodingKeys: String, CodingKey {
case title, subtitle, body
case imageURL = "image_url"
}
}
}

// MARK: - NotificationPayloadConvertible

protocol NotificationPayloadConvertible: Codable {

static var key: String { get }
}

// MARK: - NotificationPayloadConvertible+UNMutableNotificationContent

extension NotificationPayloadConvertible {

static func make(fromNotificationContent content: UNMutableNotificationContent) -> Self? {

let userInfo = content.userInfo

guard let userInfoDictionary = userInfo["data"] as? [String: Any] else {
return nil
}

guard let payloadDictionary = userInfoDictionary[Self.key] as? [String: Any] else {
return nil
}

do {
let jsonData = try JSONSerialization.data(withJSONObject: payloadDictionary, options: [])
return try JSONDecoder().decode(Self.self, from: jsonData)
} catch {
print("Error decoding JSON: \(error)")
return nil
}
}
}

Update Module's pushServiceClassName

In order to inform Q2MobileCore that your module has push notification service extension implemented, update pushServiceClassName in settings.json file:

{
"name": "q2-demo-ios",
"packageName": "q2-demo-ios",
"productName": "Q2Demo",
"className": "Q2DemoModule",
"pushServiceClassName": "DemoNotificationServiceExtensionModule",
"pushContentClassName": "DemoNotificationContentExtensionModule",
"identifier": "com.q2.mobile.module.demo",
"data": "",
"enabled": true
}

Send Notification

In addition to steps performed earlier to send simple push notification, you need to add mutable-content with number 1 next to aps node which would inform DevApp to trigger your notification service extension module. You can provide additional data node along with aps which indicates notification is catered for your module.

{
"aps": {
"alert": {
"title": "title",
"subtitle": "subtitle",
"body": "body"
},
"mutable-content": 1
},
"sender": "com.q2.mobile.module.{your-unique-identifier}",
"data": {
"demo": {
"title": "modified title",
"subtitle": "modified sub title",
"body": "modified body"
}
}
}

When you Hit send then observe notification delivered to device/simulator's content is modified by your notification service extension module.

Your screen should look as follows:

Notification Service Extension Screen

Rich Push Notification - Content Extension

The content extension gives you option similar to what you have in an app to design your notification in interface builder. Read more on: https://developer.apple.com/documentation/usernotificationsui/customizing-the-appearance-of-notifications

Implementation

Create a class inherited from UNNotificationContentExtension and conforming to PushContentExtensionModule as shown below:


import Foundation
import Q2ModuleInterfaces
import UserNotificationsUI

public class DemoNotificationContentExtensionModule: UIViewController, UNNotificationContentExtension, PushContentExtensionModule {

public var moduleDelegate: ModuleDelegate!
public var moduleDataSource: ModuleDataSource!

public static var sender: PushSender? {
return .moduleSender
}

// MARK: - Private Properties

private lazy var customLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = .black
label.numberOfLines = 0
label.textAlignment = .center
return label
}()

public func didReceive(_ notification: UNNotification) {
let content = notification.request.content
customLabel.text = content.body
}

public override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(customLabel)
NSLayoutConstraint.activate([
customLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
customLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -10),
customLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 10),
customLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10),
])
}
}

Update Module's pushContentClassName

In order to inform Q2MobileCore that your module has push notification content extension implemented, update pushContentClassName in settings.json file:

{
"name": "q2-demo-ios",
"packageName": "q2-demo-ios",
"productName": "Q2Demo",
"className": "Q2DemoModule",
"pushServiceClassName": "DemoNotificationServiceExtensionModule",
"pushContentClassName": "DemoNotificationContentExtensionModule",
"identifier": "com.q2.mobile.module.demo",
"data": "",
"enabled": true
}

Add Push Content Category

  1. Go to DevApp > PushContentExtension
  2. Select Info.plist.
  3. Under NSExtensionAttributes add UNNotificationExtensionCategory.
  4. Add name of your category and make sure document that in your project Readme as well as notify during MSDK request.

This is a mandatory step and the same category value has to be specified when you send notification otherwise DevApp won't be able to launch your notification content extension module.

Send Notification

In addition to steps performed earlier to send simple push notification, you need to add mutable-content with number 1 next to aps node which would inform DevApp to trigger your notification service extension module:

{
"aps": {
"alert": {
"title": "title",
"subtitle": "subtitle",
"body": "body"
},
"mutable-content": 1,
"category": "Q2Demo"
},
"sender": "com.q2.mobile.module.{your-unique-identifier}"
}

When you Hit send then observe notification delivered to device/simulator's content. Tap and hold notification to see modified presentation of your content via your content extension module.

Your screen should look as follows:

Notification Content Extension Screen

Troubleshooting

Common Issues

Problem: Module not receiving notifications

  • ✅ Verify sender ID matches exactly in payload and module
  • ✅ Check settings.json has "push": true and correct className
  • ✅ Ensure module is registered and enabled in Q2MobileCore
  • ✅ Verify notification payload includes sender field

Problem: Device token not received

  • ✅ Check APS Environment in entitlements (development/production)
  • ✅ Verify bundle ID matches Firebase configuration
  • ✅ Ensure remote-notification in UIBackgroundModes
  • ✅ Test on physical device (simulator limitations on older Macs)

Problem: Service extension not triggered

  • ✅ Add "mutable-content": 1 to notification payload
  • ✅ Verify pushServiceClassName in settings.json
  • ✅ Check service extension target exists in Xcode project
  • ✅ Ensure service extension timeout handling is implemented

Problem: Content extension not showing

  • ✅ Add correct category to notification payload
  • ✅ Configure UNNotificationExtensionCategory in extension's Info.plist
  • ✅ Verify pushContentClassName in settings.json
  • ✅ Check content extension target exists in Xcode project

Debugging Tips

Enable Console Logging:

// Add to your module implementation
private func logNotificationDetails(_ userInfo: [AnyHashable: Any]) {
print("Notification received: \(userInfo)")
if let sender = userInfo["sender"] as? String {
print("Sender: \(sender)")
}
}

Test with Apple Push Notification Console:

  1. Go to https://icloud.developer.apple.com/dashboard/
  2. Select Push Notifications
  3. Use your development certificate
  4. Test with minimal payload first, then add complexity

Verify Module Registration:

// Add to your module's initialization
print("Module registered with sender: \(Self.sender?.id ?? "nil")")

Best Practices

Security:

  • Always validate notification sender before processing
  • Sanitize payload data before using in UI
  • Use HTTPS for any network requests in extensions
  • Implement proper timeout handling in service extensions

Performance:

  • Keep service extension processing under 30 seconds
  • Use background tasks for long-running operations
  • Implement proper error handling and fallbacks
  • Cache resources when possible

Testing:

  • Test on multiple iOS versions and devices
  • Verify behavior with network connectivity issues
  • Test notification limits and throttling
  • Validate with both development and production certificates