iOS remote push notifications via Firebase Cloud Messaging iOS 08.07.2021

Push notifications provide a unified, consistent way to inform your app’s users of just about anything.

To receive a push notification, your device needs to be registered with the Apple Push Notification service (APNs) by receiving a unique device token. Once the device is registered, you can send push notifications to the device by sending a request to APNs using the device token.

You can implement your own web service to communicate with APNs, but there are easier options. One of those is Firebase Cloud Messaging (FCM). With Firebase Cloud Messaging (FCM), you have an easy-to-use system at your fingertips. FCM handles the cloud aspect of push notifications, letting you send and receive pushes without having to write your own web service.

Here is what you can do with APNs these days:

  • Display a message
  • Play a sound
  • Set a badge icon on your app
  • Provide actions the user can act upon with or without opening the app
  • Show an image or other type of media
  • Be silent but ask the app to perform some action in the background

Stuff needed prepared before APNS configuration can start

  • A real iOS device. Simulators cannot receive remote push notifications unfortunately.
  • A paid Apple Developer Program membership to use push notifications with your app.
  • A Google Firebase account.
  • A way to send notification payloads to your device. I'm going to use pythons script for that.

On a high-level, here are the steps to integrate Firebase push notifications in Swift 5:

  • Generate an APNs certificate on Apple’s Developer portal
  • Enable Push Notification in Firebase Cloud Messaging Console
  • Add the Swift Package Manager or CocoaPods dependencies
  • Write the code to generate the push notification token
  • Send a push notification from Firebase Notifications dashboard

Sending the notification through some Provider (like FCM / Pusher / your own back-end) looks like

ios_push_notifications.png

It is important to understand the steps between registering your app with the Apple Push Notification Service and the user actually receiving a notification.

  1. During application(_:didFinishLaunchingWithOptions:), a request is sent to APNs for a device token via registerForRemoteNotifications.
  2. APNs will return a device token to your app and call application(_:didRegisterForRemoteNotificationsWithDeviceToken:) or emit an error message to application(_:didFailToRegisterForRemoteNotificationsWithError:).
  3. The device sends the token to a provider in either binary or hexadecimal format. The provider will keep track of the token.
  4. The provider sends a notification request, including one or more tokens, to APNs.
  5. APNs sends a notification to each device for which a valid token was provided.

To tell Xcode that you’ll be using push notifications in this project, just follow these four simple steps so that it can handle the registration for you:

  1. Press ⌘ + 1 (or View ▸ Navigators ▸ Show Project Navigator) to open the Project Navigator and click on the top-most item (e.g. your project).
  2. Select the target, not the project.
  3. Select the Signing & Capabilities tab.
  4. Click the + Capability button in the top right corner.
  5. Search for and select Push Notifications from the menu that pops up.
  6. Notice the Push Notifications capability added below your signing information.

Configuring Firebase

Creating the p8 Certificate. Firebase requires you to upload a p8 certificate to your app. This is a special file containing a private key that allows Firebase to send notifications. To get a p8 certificate, sign in to Apple Developer.

Select Certificates, Identifiers & Profiles and go to Keys. Select the circle + button to create a new key.

Give it a name and enable the Apple Push Notifications service (APNs) service. Select Continue and, on the next screen, select Register.

Select Download to save the p8 file locally. You’ll need to upload this to Firebase. You cannot download this after leaving this screen.

Copy and save the Key ID to a file.

Copy and save your Apple membership ID. This is next to your name in the upper-right corner of the Membership Center or under Membership Details.

Next, go to your Firebase account and select Go to console in the upper-right corner of the page. Select Add project and follow suggests from wizard.

Then, you’ll need to configure Firebase with your Apple p8 and membership information. Within your Firebase project, select the gear next to Project Overview and choose Project settings.

Next, set up an iOS app under the General section of your project settings.

After registering your app, download GoogleServices-Info.plist. You’ll need this to configure Firebase in your app later.

Next, upload your p8 certificate by going to Cloud Messaging in your Firebase project settings. Under APNs Authentication Key, select Upload.

Adding the Package. In your projects root directory, run pod init or create a Podfile manually.

Open Podfile and add the following Firebase Pod dependencies

pod 'Firebase/Analytics'
pod 'Firebase/Messaging'

Run pod update to install the cocoa pods.

Also, you can use Swift Package Manager to add the Firebase dependency to your project. In Xcode, select File > Swift Packages > Add Package Dependency .... In the Choose Package Repository pop-up, enter https://github.com/firebase/firebase-ios-sdk.git.

Select Next, keeping the default options, until you get to a screen with a list of packages. Select the following packages from the list: FirebaseAnalytics, FirebaseMessaging.

Configuring Your App. Start by opening Info.plist and adding the following entry:

Key: FirebaseAppDelegateProxyEnabled
Type: Boolean
Value: 0

By default, FirebaseMessaging uses method swizzling to handle push notifications. You’ll handle all the code yourself, so turn this off using the plist entry you just added.

Launching Firebase. Open the AppDelegate.swift file. This is where we are going to ask the user for a permission to receive notifications.

import UIKit
import Firebase
import FirebaseMessaging

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

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

        FirebaseApp.configure()
        FirebaseConfiguration.shared.setLoggerLevel(.min)

        setupRemotePushNotifications()

        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        Logger.e("Failed to register: \(error)")
    }
}

// MARK: - Notifications

extension AppDelegate {
    private func setupRemotePushNotifications() {
        UNUserNotificationCenter.current().delegate = self

        UNUserNotificationCenter.current().requestAuthorization(
            options: [.alert, .badge, .sound],
            completionHandler: { [weak self] granted, error in
                if granted {
                    self?.getNotificationSettingsAndRegister()
                } else {
                    if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first {
                        DispatchQueue.main.async {
                            let alert = UIAlertController(title: "Notification Access", message: "In order to use this application, turn on notification permissions.", preferredStyle: .alert)
                            let alertAction = UIAlertAction(title: "Okay", style: .default, handler: nil)
                            alert.addAction(alertAction)
                            window.rootController.present(alert , animated: true)
                        }
                    }    
                }
            })

        Messaging.messaging().delegate = self

        UIApplication.shared.registerForRemoteNotifications()

        processPushToken()
    }

    private func getNotificationSettingsAndRegister() {
        UNUserNotificationCenter.current().getNotificationSettings { settings in
            guard settings.authorizationStatus == .authorized else { return }
            DispatchQueue.main.async {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
    }

    private func processPushToken() {
        if let token = Messaging.messaging().fcmToken {
            Logger.d("FCM TOKEN \(token)")
        }
    }
}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

        let ui = notification.request.content.userInfo
        let type = ui["type"] as! String

        var category = UNNotificationCategory(identifier: "", actions: [], intentIdentifiers: [], options: [])

        switch type {
        case "type1":
            let acceptAction = UNNotificationAction(identifier: "accept", title: "Accept", options: [.foreground])
            let declineAction = UNNotificationAction(identifier: "decline", title: "Decline", options: [.foreground, .destructive])

            category = UNNotificationCategory(identifier: "", actions: [acceptAction, declineAction], intentIdentifiers: [], options: [])
        default:
            break
        }

        UNUserNotificationCenter.current().setNotificationCategories([category])

        completionHandler([.alert, .badge, .sound])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

        if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first {

            let ui = response.notification.request.content.userInfo
            let type = ui["type"] as! String

            switch type {
            case "type1":
                let vc = MainTabBarController()
                vc.modalPresentationStyle = .fullScreen
                vc.selectedIndex = 0
                window.rootViewController = vc
                window.makeKeyAndVisible()
            default:
                break
            }
        }

        completionHandler()
    }
}

extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        processPushToken()
    }
}

userNotificationCenter(_:willPresent:withCompletionHandler:) gets called whenever you receive a notification while the app is in the foreground.

userNotification(_:didReceive:withCompletionHandler:) gets called when a user taps a notification.

Next, open your app’s project settings and go to Signing & Capabilities. Select the + Capability button. Search for Push Notifications in the field and press Enter.

Sending Notifications

Your app is now ready to receive notifications. Build and run on a real device. It should look the same as before, but with an alert asking for permission to send you notifications. Be sure to select Allow.

Now, you can go to your Firebase project and select Cloud Messaging found under Engage. Then select Send your first message.

Or use Firebase Admin SDK for Python.

from firebase_admin import credentials
from firebase_admin import messaging

cred = credentials.Certificate("adminsdk.json")
firebase_admin.initialize_app(cred)

registration_token = 'TOKEN'

title= 'Test title'
body= 'Test body'
ntf_data = {"type": "type1"}

alert = messaging.ApsAlert(title = title, body = body)
aps = messaging.Aps(alert = alert, sound = "default", mutable_content = True)
payload = messaging.APNSPayload(aps)

message = messaging.Message(
    notification = messaging.Notification(
        title = title,
        body = body,
        image = 'SOME_URL'
    ),
    data = ntf_data,
    token = registration_token,
    apns = messaging.APNSConfig(payload = payload)
)

response = messaging.send(message)

print('Successfully sent message:', response)

Or use curl

curl https://fcm.googleapis.com/fcm/send -X POST \
--header "Authorization: key=SERVER_KEY" \
--Header "Content-Type: application/json" \
 -d '{
   "to": "TOKEN"
   "notification":{
     "title": "New Notification!",
     "body": "Test"
     "sound": "default",
     "mutable_content" : true,
     "category" : "TestPush"
   },   
   "priority": "high",
   "content_available":false,
   "data":{
        "image-url": "IMAGE_URL"
    }
}'

Adding custom actions

Do you want to add some custom actions? You may be familiar with using this on apps such as mail or messages that allow you to swipe down on the notification and then perform a few simple tasks such as deleting a message, archiving it, or opening the app to send a reply.

Go back to the AppDelegate.swift to userNotificationCenter(_: willPresent:withCompletionHandler) function and add some actions

let acceptAction = UNNotificationAction(identifier: "accept", title: "Accept", options: [.foreground])
let declineAction = UNNotificationAction(identifier: "decline", title: "Decline", options: [.foreground, .destructive])
let category = UNNotificationCategory(identifier: "", actions: [acceptAction, declineAction], intentIdentifiers: [], options: [])

UNUserNotificationCenter.current().setNotificationCategories([category])

We create 2 UNNotificationAction objects here and initialise with an identifier, a title which is what appears on the button on the notification, and some options which are UNNotificationActionOptions which can be used to request the device is unlocked first with the .authenticationRequired constant specified, or change the text to red if .destructive is selected, and finally, .foreground which opens the app when selected. You do not have to pass constants in here, and if you don’t, the button will just have normal behaviour.

We are not ready yet, because if you think about it, we are not handling these custom actions anywhere. The place to do that is in the AppDelegate.swift file. Just add this optional func before closing the AppDelegate class:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    if let window = UIApplication.shared.windows.filter({$0.isKeyWindow}).first {        
        switch response.actionIdentifier {
        case "accept":
            print("Handle accept action identifier")
        case "decline":
            print("Handle decline action identifier")        
        default:
            break
        }
    }

    completionHandler()
}

Also you can add Text Input to a notification.

Adding media attachments

Up to this point, your notifications have all contained only text. But if you’ve received many notifications, you know that notifications can have rich content, such as images. It’d be great if your notifications showed users a nice image related to their content.

To show an image in push notifications, you’ll need to create a Notification Service Extension. This is a separate target in your app that runs in the background when your user receives a push notification. The service extension can receive a notification and change its contents before iOS shows the notification to the user.

Go to File > New > Target. Then in the next menu, under iOS, Filter "service" and choose Notification Service Extension.

Click the Next button and give it a name of your choosing. I am going to name mine NotificationService. Click Finish. What we are going to get is a new group of two files created for us.

Go ahead and open the NotificationService.swift file. The didReceive(_:withContentHandler:) function is the one that is the more important because that is where we can modify the notification to our liking.

Before that, we need to make sure we add this key-value pair if we actually want to use a notification extension of any kind:

mutable-content: 1

It signifies that the OS should initiate the service extension of the app and do some extra processing.

Look at python example above.

Open Podfile and add the following Firebase Pod dependencies

target 'MainApp' do
  use_frameworks!

  # Firebase
  pod 'Firebase/Analytics'
  pod 'Firebase/Messaging'  
end

target 'NotificationService' do
  use_frameworks!

  pod 'Firebase/Messaging'
end

The final NotificationService.swift looks like

import UserNotifications
import FirebaseMessaging

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

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        if let bestAttemptContent = bestAttemptContent {
            FIRMessagingExtensionHelper().populateNotificationContent(
                bestAttemptContent,
                withContentHandler: contentHandler)
        }
    }

    override func serviceExtensionTimeWillExpire() {
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}

Typically, you’d have to search out the field that contains the image URL, download the image, and then finish the presentation with the image as an attachment. Here, you’re using Firebase’s FIRMessagingExtensionHelper to automatically perform all that work in one simple helper method call.

Keep in mind, iOS only gives you a limited amount of time to download your attached image. If the extension’s code takes too long to run, the system will call serviceExtensionTimeWillExpire(). This gives you a chance to gracefully finish up anything you are doing in the extension or to simply present the notification as is, which is the default implementation.