Implementing removable remote notifications on iOS

Most of the apps we create have some form of notifications. They can be either local or remote – sent from backend via APNS. There are some cases where we would want to remove notification from user’s phone after some condition is met, e.g.

  • It’s not valid anymore, e.g. we sent a notification with an offer which ended before user read it.
  • We have a chat application and user already read the message on another device or our website.

Fortunately, it’s suprisingly easy to do with UserNotifications framework introduced with iOS10.

To try this yourself, you need active Apple Developer account, as working with push notifications requires that.

Firstly, create new iOS app Xcode project. It doesn’t matter what kind, we won’t leave AppDelegate.swift file :). After creating the project, turn on Push Notifications in Capabilities section of your target settings. You’ll also need to turn on Background Modes capability and check Remote notifications mode. One last step is to create Sandbox APNS certificate in Apple Developer Portal. Now, let’s jump into the code. Replace your AppDelegate with this snippet:

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        application.registerForRemoteNotifications()

        UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { (status, error) in
            print(status)
            print(error)
        }

        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let hexString = deviceToken.map { String(format: "%02hhx", $0) }.joined()
        print(hexString)
    }

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        guard let idToDelete = userInfo["del-id"] as? String else {
            completionHandler(.noData)
            return
        }

        UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [idToDelete])
        completionHandler(.noData)
    }
}

Let’s go through it. In application:didFinishLaunchingWithOptions: we just register for notifications and prompt for authorization from user. application:didRegisterForRemoteNotificationsWithDeviceToken: converts device token to hex form and prints it. This will allow us to send pushes from our command line. For now, let’s skip application:didReceiveRemoteNotification:fetchCompletionHandler: as it doesn’t make much sense without some introduction. UNUserNotificationCenter has a method which allows us to remove delivered notifications with specified ids. It’s easy when dealing with local notifications, as we are responsible for providing identifiers for them. On the other hand, remote notifications have random identifiers generated by APNS. There is only one exception from that — if we specify apns-collapse-id header in our request to APNS, it will send notification with identifier equal to value of this header. Normally, apns-collapse-id is used to create notifications which should be replaced with newer ones, but this nice behavior (which is documented, so don’t worry ;)) will allow us to send notification with specific id. To do that, just run some cURL command in your Terminal:

curl -v \
-d '{"aps": {"alert": "This notification will be removed"}}' \
-H "apns-topic: <YOUR APP BUNDLE ID>" \
-H "apns-collapse-id: 1234" \
--http2 \
--cert <PATH TO YOUR CERTIFICATE IN PEM FORMAT> \https://api.development.push.apple.com/3/device/<DEVICE TOKEN>

This will send notification with identifier set to 1234. Great, we are halfway done! Now we just need to trigger our app to remove specified notification on demand. And now we can go back to application:didReceiveRemoteNotification:fetchCompletionHandler: method. This method is always called when app receives push notification with special field in its payload: content-available with value 1 (why not true? Ask Apple). Our implementation searches notification’s payload for del-id field which contains id of notification to delete. With that id available, we can remove notification with UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [idToDelete]). Now, our app is ready to remove notifications from users’ phones! Just send triggering push with del-id set to our previous notification’s id — 1234. This notification doesn’t have any alert or sound so it won’t be visible to user.

curl -v \
-d '{"aps": {"content-available": 1}, "del-id": "1234"}' \
-H "apns-topic: <YOUR APP BUNDLE ID>" \
--http2 \
--cert <PATH TO YOUR CERTIFICATE IN PEM FORMAT> \https://api.development.push.apple.com/3/device/<DEVICE TOKEN>

We are done! Let’s enjoy our creation:

I hope this short tutorial will be useful to you, but don’t overuse this, you users might get confused when their notifications start to disappear. Cheers!

comments powered by Disqus