Service Extensions are intended to act as middleware for push notifications. The Service Extension receives the notification before it's displayed to the user. This allows you to manipulate or enrich the content before it's displayed.
A Service Extension is perfect if you're implementing end-to-end encryption, for example. Before this extension was introduced in iOS 10, notifications had to be sent in plain text. This means that any app that implements end-to-end encryption still wasn't completely secure because push notifications were not encrypted. Since iOS 10, you can push the encrypted message from your server to the device and decrypt the message in the Service Extension before showing it to the user.
Another great use for a Service Extension is to download media attachments from a push notification, save it locally, and add it as a media attachment to the notification contents. Remember that all media attachments for notifications must be stored locally on the device. This means that a push notification can't really have media attachments unless a Service Extension is used to download and store the media locally.
To allow a Service Extension to handle a push notification, you must add the mutable-content property to the aps dictionary on the notification:
{
aps: {
alert: “You have a new message!”,
badge: 1,
mutable-content: 1
},
custom-encrypted-message: "MyEncryptedMessage"
}
When the mutable-content property is detected by iOS, your Service Extension is activated and receives the notification before it's displayed to the user. A service extension is created in the same way as other extensions. You go to the project settings in Xcode and, in the sidebar that shows all targets, you click the plus icon. In the dialog that appears, you select the Notification Service Extension, you give it a name, and Xcode will provide you with the required boilerplate code. When you add a Service Extension, a sample extension is added to The Daily Quote to illustrate what a Service Extension that updates the notification's body text looks like.
To apply this service extension to The Daily Quote, the service extension must receive access to the app group we created earlier and the Quote.swift file should be added to the extension target. This enables the extension to read quotes from the shared UserDefaults.
The boilerplate code that Xcode generates for a Service Extension is rather interesting. Two properties are created, a contentHandler and bestAttemptContent. The properties are initially given a value in didReceive(_:withContentHandler:). This method is called as soon as we're expected to handle the notification.
If we fail to call the callback handler in a timely manner, the system calls serviceExtensionTimeWillExpire(). This is essentially our last chance to quickly come up with content for the notification. If we still fail to call the callback, the message is displayed to the user in its original form. Xcode generated a version of this method for us that uses the stored callback and the current state that our notification is in, and the callback is called immediately.
Let's look at a simple implementation of didReceive(_:withContentHandler:):
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
let todayQuote = Quote.current
bestAttemptContent.subtitle = "Quote by \(todayQuote.creator)"
bestAttemptContent.body = todayQuote.text
contentHandler(bestAttemptContent)
}
}
In this snippet, we grab the incoming notification contents and we add a subtitle and a body to them. The quote struct is used to obtain the current quote from UserDefaults. This approach enables us to push a fresh quote to users every day, even if the server has no idea which quote should be displayed today. Pretty awesome, right?