Implementing Rich Notifications in iOS 10
I’ve spent a bit of time with notification extensions in iOS 10 and they’re pretty great. If you’re not familiar with notification extensions, check out Apple’s Introduction to Notifications talk before reading this post. Anywho, notification extensions improve your app’s notification experience giving you a chance to mutate notification payloads, download and display media, and show custom interfaces in response to notifications. I thought I’d share a bit about how they work and a few things I ran into while adding notification extensions to an existing app.
Before we dive in, I should mention that iOS 10 brings other fancy notification enhancements like aggregation, a way to handle notification actions in an extension, and the ability to opt into system notification banners for your application while it is open. I’ve chosen to focus on processing and displaying notifications in this post so I won’t get to any of those things. Okay, on to the good stuff.
iOS 10 brings two new notification extensions: Service and Content. Service extensions let you mutate notification payloads and Content extensions give you a way to show a custom interface in response to notifications. Incidentally, Content extensions also allow you to handle custom actions.
Service Extensions #
Service extensions give you a chance to modify notification payloads before they are displayed by the system. For instance, you might decrypt some text in the payload on the client before displaying it or download an image, video, or GIF, and attach it to the notification itself. Or you might set a localized title and subtitle to be shown in the notification banner.
A Service extension’s entry point class is a subclass of
UNNotificationServiceExtension that overrides two methods:
func didReceive(UNNotificationRequest, withContentHandler: @escaping (UNNotificationContent) -> Void) and
func serviceExtensionTimeWillExpire(). The system starts your Service extension when a notification payload with the “mutable-content” key set to 1 (in the “aps” dictionary) is received.
didReceive(_:withContentHandler:) is then called and passed an instance of
UNNotificationRequest representing the notification received and a completion block to call when you’re finished processing the payload. You should assign both to instance variables because you will need to access them elsewhere if you take too long to process a notification.
The system gives you a limited amount of time to process a notification.
serviceExtensionTimeWillExpire() is called if you fail to call the completion handler in time. This is your last opportunity to wrap up processing and the completion block with the updated notification content. For instance, if you’ve downloaded two of four images you would call back with those two images attached and cancel your remaining downloads.
One important thing I recently learned is you cannot modify silent notifications (e.g. “content-available” or notifications that play a sound or badge the icon without including an alert key) using a Service extension. Apple calls this out clearly in their documentation.
You can query the notification request’s content property, which is of type
UNMutableContent. You can make a mutable instance of this object by calling
mutableCopy(). Now you can modify all of the basic properties of a notification including the title, subtitle, body, badge count, and userInfo in the Service extension. Note that if you set the body text to an empty string, the system will ignore changes you made to the notification content and render the original alert from the payload text with a default UI instead.
You can also fetch and attach audio, images, GIFs, or movies to the instance of
UNMutableNotificationContent you were passed earlier. Once you’ve downloaded media to disk, you can to initialize an instance of UNNotificationAttachment with a string identifier, the file URL, and options (if needed) and attach it to the notification content.
Audio downloads are currently limited to 5MB, images to 10MB, and video to 50MB. These are rather large limits so I recommend serving small files in an effort to be respectful of your user’s bandwidth.
The process of downloading multiple files is well documented elsewhere, but in short you can use dispatch_groups or NSOperation to wait until all downloads are complete then call the system provided completion block with the notification content.
I suggest using the remote URL’s absoluteString as the identifier. This way, you can easily map the remote URL from your notification payload to the attachment identifier for the attachment you downloaded at a later time in your Content extension if needed. One way to do this is to share models between your Service and Content extension.
You must create the URL passed to
UNMutableNotificationContent‘s initializer with
NSURL.fileURL(withPath:, isDirectory:) or the corresponding initializer. You’ll receive an error about an invalid attachment at run time if you create a URL with the on-disk path with the default NSURL initializer.
Like other extensions, you can set up app groups and keychain sharing to share data between the extension and your host app. For instance, you could add downloaded media to a shared, on-disk cache for your host app to consume or vice versa.
iOS provides a default interface to display media that supports displaying one media item at a time. You’ll want to write a Content extension if you want to show more than one image at a time or show a custom interface.
Content Extensions #
Content extensions let your show a custom interface for a given notification payload. The system starts your Content extension when a notification with the “category” key (in the “aps” dictionary) is set to a value registered at build time in your extension’s info.plist (via the
UNNotificationExtensionCategory key). This value can be a string or an array containing many categories. You can technically bundle multiple Content extensions with your app to handle different categories of notifications, but provisioning overhead (profiles, code signing, etc.) can make this painful. This might be useful if processing different types of notification is very different.
A Content extension’s entry point is a UIViewController subclass that implements the
UNNotificationContentExtension protocol. There’s one required method:
func didReceive(_ notification: UNNotification), which is called when the user expands a notification banner by 3D Touching the banner, pulling down on it when it slides down from the top of the screen, or by tapping the View button after swiping from right to left in Notification Center.
Otherwise, it’s like working with any other view controller. Mostly.
If you intend to have a completely custom interface, including labels for the title, subtitle, and body text then you must set
UNNotificationExtensionDefaultContentHidden to true in your Content extension’s the info.plist. Otherwise, the Apple provided UI will appear below your custom view. Frankly, I’d stick to Apple’s text views unless you don’t mind text layout, which can be pretty painful even when using stack views. Either way, be sure to test your layouts on devices with varying screen sizes to make sure your code behaves the way you expect it to.
The system animates the notification from a height based on a ratio specified by the
UNNotificationExtensionInitialContentSizeRatio key in your Content extension’s info.plist. For instance, if you set this to 0.3, the initial height of the expanded notification will be 0.3 * the content width of the notification view. The notification will then animate to the height you’ve specified as the
preferredContentSize or whatever the intrinsic content size of your view is. If your notification collapses and you only see the Apple provided banner (or the banner and Apple’s text) then it’s likely you’ve set a height of 0 for your view. Check your constraints and pray.
If you’ve attached media to the notification content, you can access it for use in your UI with one additional step. You need to call
startAccessingSecurityScopedResource() on the attachment’s URL before consuming the media referenced by an attachment. When you’re done accessing the media, you must call
stopAccessingSecurityScopedResource() or your extension will leak kernel resources! In my experience the best place to call the latter is in
dealloc() because bad and strange things happen™ if you call
stopAccessingSecurityScopedResource() too early. For instance, images may render incorrectly or not at all.
stopAccessingSecurityScopedResource() on a URL you have not called
startAccessingSecurityScopedResource() on is a no-op. This means iterating over all attachment URLs to call
stopAccessingSecurityScopedResource() is perfectly safe.
Random Stuff #
A general debugging tip: I found I needed to build and run my host app’s scheme to deploy the app and extension, click stop, then build and run the extension’s scheme to make Xcode properly deploy the updated appex to my device after making code changes to the extension. It’s possible I was doing something wrong, but keep this in mind if it looks like your code changes aren’t taking effect after building and running your extension’s scheme.
I also ran into an interesting (and rare) issue. Sometimes Notification Center stops appearing in response to a swipe down from the top of the screen. I’ve noticed notification banners also stop appearing when this happens.
Delete Reboot your device to fix both.
Drop me a note on Twitter (@amdev) if you found this post helpful.