Handling iOS push notifications

Usage guides to help you get started with the Push Notifications in the iOS Knock SDK.

Note: We Recommend taking advantage of our KnockAppDelegate to make managing your Push Notifications simpler.

Prerequisites

Before proceeding, ensure you've configured push notifications within your Knock account. For guidance on this initial setup, refer to our Push Notification Configuration Guide.

Step 1: Enabling Push Notifications in Your App

  1. Configure APNs in Your App:
    • Open your project in Xcode.
    • Navigate to your app target's Signing & Capabilities tab.
    • Click the "+" capability button and add Push Notifications to enable Apple Push Notification service (APNs).
  2. Enable Background Modes:
    • Still in the Signing & Capabilities tab, add the Background Modes capability.
    • Check Remote notifications to allow your app to receive silent push notifications.

Step 2: Registering for Push Notifications

Implement the following in your AppDelegate or SceneDelegate to register for push notifications:

KnockAppDelegate:

If using the KnockAppDelegate, this will be handled for you automatically.

Manually:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class MyAppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        UNUserNotificationCenter.current().delegate = self
		// Will request push notification permissions, and will automatically register if perms are granted.
        Knock.shared.requestAndRegisterForPushNotifications()

        // Check if launched from the tap of a notification
        if let launchOptions = launchOptions,
           let userInfo = launchOptions[.remoteNotification] as? [String: AnyObject] {
            pushNotificationTapped(userInfo: userInfo)
        }

        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        // Register the token with Knock
        Task {
            let channelId = await Knock.shared.getPushChannelId()

            do {
                let _ = try await Knock.shared.registerTokenForAPNS(channelId: channelId, token: Knock.convertTokenToString(token: deviceToken))
            } catch {
               // Handle error
            }
        }
    }
}

Step 3: Updating the Message Status of a Push Notification.

If a push notification is sent via Knock, it will contain a knock_message_id property that includes the corresponding message Id. This can then be used to update the message status.

KnockAppDelegate:

If using the KnockAppDelegate, this will be handled for you automatically.

Manually:

1
2
3
4
5
6
7
8
class AppDelegate: UIResponder, UIApplicationDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        if let messageId = getMessageId(userInfo: notification.request.content.userInfo) {
            Knock.shared.updateMessageStatus(messageId: messageId, status: .seen) { _ in }
        }
        completionHandler(presentationOptions)
    }
}

Step 4: Configuring Silent Push Notifications

Silent push notifications allow your app to update content in the background without alerting the user. Ensure that your Knock APNs message template has silent notifications enabled.

Message Template Settings

KnockAppDelegate:

1
2
3
4
5
6
class MyAppDelegate: KnockAppDelegate {
     override func pushNotificationDeliveredSilently(userInfo: [AnyHashable : Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
         // Pull any information you need out of userInfo, and change the completionHandler value depending on your needs.
         completionHandler(.noData)
     }
}

Manually:

1
2
3
4
5
6
class MyAppDelegate: UIResponder, UIApplicationDelegate {
    open func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        // Pull any information you need out of userInfo, and change the completionHandler value depending on your needs.
         completionHandler(.noData)
    }
}

Full Example

Here's the complete AppDelegate implementation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class AppDelegate: KnockAppDelegate {

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        Task {
            try? await Knock.shared.setup(publishableKey: Utils.publishableKey, pushChannelId: Utils.apnsChannelId, options: .init(hostname: Utils.hostname, loggingOptions: .verbose))
        }
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    override func pushNotificationTapped(userInfo: [AnyHashable : Any]) {
        super.pushNotificationTapped(userInfo: userInfo)
        if let deeplink = userInfo["link"] as? String, let url = URL(string: deeplink) {
            UIApplication.shared.open(url)
        }
    }

    override func pushNotificationDeliveredInForeground(notification: UNNotification) -> UNNotificationPresentationOptions {
        let options = super.pushNotificationDeliveredInForeground(notification: notification)
        // Handle push notification here
        return [options]
    }

    override func pushNotificationDeliveredSilently(userInfo: [AnyHashable : Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        // Handle silent push notification here
        completionHandler(.noData)
    }
}

Using FCM in iOS

If you prefer to use Firebase Cloud Messaging (FCM) within your iOS app, you can follow the same steps outlined above for push notifications with a few modifications to your AppDelegate.

Step 1: Configure Firebase

In your application(_:didFinishLaunchingWithOptions:) method, add the following:

  1. Initialize Firebase:

    1
    FirebaseApp.configure()
  2. Set the Messaging Delegate:

    1
    Messaging.messaging().delegate = self
  3. Setup Knock:

    1
    2
    3
    4
    5
    6
    7
    Task {
        try? await Knock.shared.setup(
            publishableKey: Utils.publishableKey,
            pushChannelId: Utils.apnsChannelId,
            options: .init(hostname: Utils.hostname, loggingOptions: .verbose)
        )
    }

Step 2: Override Push Notification Registration Methods

Override the application(_:didRegisterForRemoteNotificationsWithDeviceToken:) method to prevent the KnockAppDelegate from handling this automatically:

1
override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {}

Step 3: Implement MessagingDelegate

Extend your AppDelegate to conform to MessagingDelegate and implement the messaging(_:didReceiveRegistrationToken:) method:

1
2
3
4
5
6
7
8
9
extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        Task {
            if let channelId = await Knock.shared.getPushChannelId(), let token = fcmToken {
                let _ = try? await Knock.shared.registerTokenForAPNS(channelId: channelId, token: token)
            }
        }
    }
}

Full Example

Here's the complete AppDelegate implementation with FCM support:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class AppDelegate: KnockAppDelegate {

    override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // Step 1: Configure Firebase and Knock
        FirebaseApp.configure()
        Messaging.messaging().delegate = self

        Task {
            try? await Knock.shared.setup(
                publishableKey: Utils.publishableKey,
                pushChannelId: Utils.apnsChannelId,
                options: .init(hostname: Utils.hostname, loggingOptions: .verbose)
            )
        }
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }

    // Step 2: Override registration for remote notifications
    override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {}

    override func pushNotificationTapped(userInfo: [AnyHashable : Any]) {
        super.pushNotificationTapped(userInfo: userInfo)
        if let deeplink = userInfo["link"] as? String, let url = URL(string: deeplink) {
            UIApplication.shared.open(url)
        }
    }

    override func pushNotificationDeliveredInForeground(notification: UNNotification) -> UNNotificationPresentationOptions {
        let options = super.pushNotificationDeliveredInForeground(notification: notification)
        return [options]
    }

    override func pushNotificationDeliveredSilently(userInfo: [AnyHashable : Any], completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        completionHandler(.noData)
    }
}

// Step 3: Implement MessagingDelegate
extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        Task {
            if let channelId = await Knock.shared.getPushChannelId(), let token = fcmToken {
                let _ = try? await Knock.shared.registerTokenForAPNS(channelId: channelId, token: token)
            }
        }
    }
}