App Extensions: Introduction to Notification Service

I’m happy to bring you a small introduction to App Extensions, focusing a bit on Notification Service.

The goal is to provide you with a brief introduction. We are going to go step by step on how to add a new app extension, tips for simulating push notifications on your real device and advice in case you face similar issues we had.

APP EXTENSIONS

I believe this quote from Apple’s official documentation summarizes the goal of App Extensions pretty well:

"App extensions let you extend custom functionality and content beyond your app and make it available to users while they’re interacting with other apps or the system."

Today, we have 36 different types of App Extensions templates, where 29 of them can be added to our iOS/iPadOS apps, allowing us to add custom extra functionality to areas like Notifications, Sharing, Siri Interactions and Messaging between others.

THE NOTIFICATION SERVICE EXTENSION

The Notification Service Extension was designed to intercept any incoming push notification our applications receive, allowing us to modify its payload content— for example, changing the title, decrypting any encrypted data or even downloading media attachments.

ADDING THE NOTIFICATION SERVICE EXTENSION

Once you have your App project on Xcode, simply do File -> New -> Target, then select Notification Service Extension:

Screen-Shot-2021-09-30-at-3.11.27-AM

After choosing the template, you'll need to fill a couple of fields adding information to your extension such as Product Name, Team, Language, Project and Embedded in Application:

Screen-Shot-2021-09-30-at-3.11.45-AM

After adding it, you will have a new folder for your extension. Inside it, you will find a new file containing boilerplate code and an info.plist file. You should also have a new product inside the Products folder referring to the new app extension; its extension should be .appex (in case you have the product with a different extension other than .appex, check the troubleshooting section).

MANIPULATING THE NOTIFICATION PAYLOAD

The manipulation of the data in the payload is quite simple, but it will mostly depend on your use case. In any case, the boilerplate code is a good starting point for a better understanding. You will see that our NotificationsService.swift is a class that conforms to UNNotificationServiceExtension protocol, which has two methods:

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

open func serviceExtensionTimeWillExpire()

The first method, as the official documentation says: Call contentHandler with the modified notification content to deliver. So, this is the place where we will work on preparing our new payload with our custom logic.

And the second method is called just before the extension will be terminated by the system. We may use this as an opportunity to deliver our "best attempt" at modified content; otherwise, the original push payload will be used.

The boilerplate code already has both methods added and the basic implementation should be something like the following:

import UserNotifications

class NotificationService: UNNotificationServiceExtension {

	var contentHandler: ((UNNotificationContent) -> Void)?      
    var bestAttemptContent: UNMutableNotificationContent?

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

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

	guard                
    	let bestAttemptContent = bestAttemptContent          
    else {                
    	return           
    }           
    
    // 2           
    bestAttemptContent.title = "\(bestAttemptContent.title) 
[modified]"     
    bestAttemptContent.subtitle = "\(bestAttemptContent.subtitle
[modified]"           
    bestAttemptContent.badge = 1           
    bestAttemptContent.sound = UNNotificationSound(named:
UNNotificationSoundName.MySoundName)     

    // 3           
    if var customDictionary =
bestAttemptContent.userInfo["my_custom_key"] 
as? [AnyHashable: Any] {       
	}           
    
    // 4           
    contentHandler(bestAttemptContent)      
  }      
    
  override func serviceExtensionTimeWillExpire() {           
  
 	// 5          
 	if let contentHandler = contentHandler, let 
    bestAttemptContent = bestAttemptContent {                
    	contentHandler(bestAttemptContent)           
     }     
   }
}

1. We set the local contentHandler and bestAttemptContent local properties with the received ones and, right after, we safely unwrap the bestAttemptContent in order to have it as non-optional.

2. The bestAttemptContent is the object where we can manipulate the payload fields, like title, subtitle , badge, sound between others.

3. We can also access custom fields using the userInfo.

4. Then we call the contentHandler with our new bestAttemptContent object.

5. In this case, we are using the boilerplate implementation for the serviceExtensionTimeWillExpire() method, which will unwrap the contentHandler and bestAttemptContent and will try to deliver the "best attempt" of the modified content, as mentioned before.

Also, it is worth noting that since iOS 13.3, we now have the capability to enable receiving notifications without displaying the notification to the user. With this, we'll be able to receive and analyze the notification and, based on our use case, we can simply discard the notification. This can be achieved by adding com.apple.developer.usernotifications.filtering entitlement key to the Notification Service Extension target entitlements file, with value YES because it is a boolean type. Then, in case you don't want to display a notification to the user, you can call the completion handler passing a new UNNotificationContent instance after checking if you either want to display it or not.

// This will not deliver the notification to the
user.contentHandler(UNNotificationContent())

HOW TO TEST

Within the latest Xcode versions, we had received the capability to either drop an .apns file containing our notification payload to the simulator or use the console to send the notifications to our device or simulator. Then we could test the received push notifications quite easily. But, unfortunately, we can't test the notification service extension on simulators; it simply doesn't work. The extension never gets called.

The good news is that we can quite easily test simulating push notifications in our real device using either PushNotifications tester or curl commands. It's pretty straightforward to use them, we only need to have either the .p12 certificate or the .p8 token for authentication, along with the app bundle id, device notification token and the push notification payload.

For using the PushNotifications tester, just download it and fill out the information it needs (as mentioned above). But, if you want to test using curl, you can do it by using the following commands:

// Certificate based push notification
% curl -v --header "apns-topic: ${TOPIC}" --header 
"apns-push-type: alert" --cert "${CERTIFICATE_FILE_NAME}" 
--cert-type DER --key "${CERTIFICATE_KEY_FILE_NAME}" 
--key-type PEM --data '<Push notification payload>' 
--http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}

// Token based push notification
% curl -v --header "apns-topic: $TOPIC" --header 
"apns-push-type: alert" --header "authorization: bearer
$AUTHENTICATION_TOKEN" --data '<Push notification 
payload>' 
--http2 https://${APNS_HOST_NAME}/3/device/${DEVICE_TOKEN}

Remember that our push notification payload must satisfy two requirements for triggering the notifications service extension. First, we need to have the alert key, which will display the alert notification. Second, at the same level, we must include "mutable-content" key with value 1 , both inside aps key. See the example below.

{ 	    
	"aps": { 		        
    	"alert": { 			            
        	"title": "Notification Title",             
            "body": "Notifications body"        
         },        
         "mutable-content": 1    
     }
}

Remember that you must be already registered for receiving push notifications in your app, in case you haven't asked for the permission yet, just call:

let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert, .sound, 
.badge]) { granted, error in 	    
	// Handle permission granted or error 
}

TROUBLESHOOTING

One problem that could happen when adding a new extension to a project already running for a couple of years is that we can get wrong values in the extension Build Settings. In such situations, make sure you have the keys Wrapper Extension as .appex and Executable Extension as empty. In case you have these keys with the wrong value, you will still be able to run the project and the extension target in your device — but the extension code will never be executed.

Screen-Shot-2021-09-30-at-3.12.01-AM


Another issue could come up if we don’t have the Alerts notification permission enabled on system settings. If the user has only Sounds and Badges enabled, the extension won't work.

Screen-Shot-2021-09-30-at-3.12.16-AM

And last but not least, guarantee that your extensions bundle identifiers are based on your main app target bundle identifier. For example, if your app bundle id is com.example.myApp your app extension must be com.example.myApp.MyAppExtension.

Screen-Shot-2021-09-30-at-3.12.25-AM

CONCLUSION

Working with app extensions could be a bit confusing at the beginning, but it can absolutely be really fun and useful. You definitely won’t need to have all of them in your project, but I believe you can always find an extension that will improve your app experience a lot by bringing new possibilities for your users.

I hope you’ve enjoyed reading these tips and tricks, that's all from me.

SOURCES

Share Article
Breno Valadão

Breno Valadão

iOS developer

You might also like...