Preparing Your App For iOS 12 Notifications
Preparing Your App For iOS 12 Notifications
Kaya Thomas2018-09-05T13:30:35+02:002018-09-05T11:52:50+00:00
In 2016, Apple announced a new extension that will allow developers to better customize their push and local notifications called the UNNotificationContentExtension
. The extension gets triggered when a user long presses or 3D touches on a notification whenever it is delivered to the phone or from the lock/home screen. In the content extension, developers can use a view controller to structure the UI of their notification, but there was no user interaction enabled within the view controller — until now. With the release of iOS 12 and XCode 10, the view controller in the content extension now enables user interaction which means notifications will become even more powerful and customizable.
At WWDC 2018, Apple also announced several changes to notification settings and how they appear on the home screen. In an effort to make users more aware of how they are using apps and allowing more user control of their app usage, there is a new notification setting called “Deliver Quietly.” Users can set your app to Delivery Quietly from the Notification Center, which means they will not receive banners or sound notifications from your app, but they will appear in the Notification Center. Apple using an in-house algorithm, which presumably tracks often you interact with notifications, will also ask users if they still want to receive notifications from particular apps and encourage you to turn on Deliver Quietly or turn them off completely.
Notifications are getting a big refresh in iOS 12, and I’ve only scratched the surface. In the rest of this article, we’ll go over the rest of the new notification features coming to iOS 12 and how you can implement them in your own app.
Recommended reading: WWDC 2018 Diary Of An iOS Developer
Remote vs Local Notifications
There are two ways to send push notifications to a device: remotely or locally. To send notifications remotely, you need a server that can send JSON payloads to Apple’s Push Notification Service. Along with a payload, you also need to send the device token and any other authentication certificate or tokens that verify your server is allowed to send the push notification through Apple. For this article, we focus on local notifications which do not need a separate server. Local notifications are requested and sent through the UNUserNotificationCenter
. We’ll go over later how specifically to make the request for a local notification.
In order to send a notification, you first need to get permission from the user on whether or not they want you to send them notifications. With the release of iOS 12, there are a lot of changes to notification settings and permissions so let’s break it down. To test out any of the code yourself, make sure you have the Xcode 10 beta installed.
Notification Settings And Permissions
Deliver Quietly
Delivery Quietly is Apple’s attempt to allow users more control over the noise they may receive from notifications. Instead of going into the settings app and looking for the app whose notification settings you want to change, you can now change the setting directly from the notification. This means that a lot more users may turn off notifications for your app or just delivery them quietly which means the app will get badged and notifications only show up in the Notification Center. If your app has its own custom notification settings, Apple is allowing you to link directly to that screen from the settings management view pictured below.
In order to link to your custom notification setting screen, you must set providesAppNotificationSettings
as a UNAuthorizationOption
when you are requesting notification permissions in the app delegate.
In didFinishLaunchingWithOptions
, add the following code:
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .providesAppNotificationSettings]) { ... }
When you do this, you’ll now see your custom notification settings in two places:
- If the user selects
Turn Off
when they go to manage settings directly from the notification; - In the notification settings within the system’s Settings app.
You also have to make sure to handle the callback for when the user selects on either way to get to your notification settings. Your app delegate or an extension of your app delegate has to conform to the protocol UNUserNotificationCenterDelegate
so you can then implement the following callback method:
func userNotificationCenter(_ center: UNUserNotificationCenter, openSettingsFor notification: UNNotification?) {
let navController = self.window?.rootViewController as! UINavigationController
let notificationSettingsVC = NotificationSettingsViewController()
navController.pushViewController(notificationSettingsVC, animated: true)
}
Another new UNAuthorizationOption
is provisional authorization. If you don’t mind your notifications being delivered quietly, you can set add .provisional
to your authorization options as shown below. This means that you don’t have to prompt the user to allow notifications — the notifications will still show up in the Notification Center.
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .provisional]) { ... }
So now that you’ve determined how to request permission from the user to deliver notifications and how to navigate users to your own customized settings view, let’s go more into more detail about the actual notifications.
Sending Grouped Notifications
Before we get into the customization of the UI of a notification, let’s go over how to make the request for a local notification. First, you have to register any UNNotificationCategory
, which are like templates for the notifications you want to send. Any notification set to a particular category will inherit any actions or options that were registered with that category. After you’ve requested permission to send notifications in didFinishLaunchingWithOptions
, you can register your categories in the same method.
let hiddenPreviewsPlaceholder = "%u new podcast episodes available"
let summaryFormat = "%u more episodes of %@"
let podcastCategory = UNNotificationCategory(identifier: "podcast", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenPreviewsPlaceholder, categorySummaryFormat: summaryFormat, options: [])
UNUserNotificationCenter.current().setNotificationCategories([podcastCategory])
In the above code, I start by initiating two variables:
hiddenPreviewsPlaceholder
This placeholder is used in case the user has “Show Previews” off for your app; if we don’t have a placeholder there, your notification will show with only “Notification” also the text.summaryFormat
This string is new for iOS 12 and coincides with the new feature called “Group Notifications” that will help the Notification Center look a lot cleaner. All notifications will show up in stacks which will be either representing all notifications from the app or specific groups that the developer has set for there app.
The code below shows how we associate a notification with a group.
@objc func sendPodcastNotification(for podcastName: String) {
let content = UNMutableNotificationContent()
content.body = "Introducing Season 7"
content.title = "New episode of (podcastName):"
content.threadIdentifier = podcastName.lowercased()
content.summaryArgument = podcastName
content.categoryIdentifier = NotificationCategoryType.podcast.rawValue
sendNotification(with: content)
}
For now, I’ve hardcoded the text of the notification just for the example. The threadIdentifier
is what creates the groups that we show as stacks in the Notification Center. In this example, I want the notifications grouped by podcast so each notification you get is separated by what podcast it’s associated with. The summaryArgument
matches back to our categorySummaryFormat
we set in the app delegate. In this case, we want the string for the format: "%u more episodes of %@"
to be the podcast name. Lastly, we have to set the category identifier to ensure the notification has the template we set in the app delegate.
func sendNotification(for category: String, with content: UNNotificationContent) {
let uuid = UUID().uuidString
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let request = UNNotificationRequest(identifier: uuid, content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
}
The above method is how we request the notification to be sent to the device. The identifier for the request is just a random unique string; the content is passed in and we create the content in our sendPodcastNotification
method, and lastly, the trigger is when you want the notification to send. If you want the notification to send immediately, you can set that parameter to nil.
Using the methods we’ve described above, here’s the result on the simulator. I have a button that has the sendPodcastNotification
method as a target. I tapped the button three times to have the notifications sent to the device. In the first photo, I have “Show Previews” set to “Always” so I see the podcast and the name of the new episodes along with the summary that shows I have two more new episodes to check out. When “Show Previews” is set to “Never,” the result is the second image above. The user won’t see which podcast it is to respect the “No Preview” setting, but they can still see that I have three new episodes to check out.
Notification Content Extension
Now that we understand how to set our notification categories and make the request for them to be sent, we can go over how to customize the look of the notification using the Notification Service and Notification Content extensions. The Notification Service extension allows you to edit the notification content and download any attachments in your notification like images, audio or video files. The Notification Content extension contains a view controller and storyboard that allows you to customize the look of your notification as well as handle any user interaction within the view controller or taps on notification actions.
To add these extensions to your app go File ? New ? Target.
You can only add them one at a time, so name your extension and repeat the process to add the other. If a pop-up appears asking you to activate your new scheme, click the “Activate” button to set it up for debugging.
For the purpose of this tutorial, we will be focusing on the Notification Content Extension. For local notifications, we can include the attachments in the request, which we’ll go over later.
First, go to the Info.plist file in the Notification Content Extension target.
The following attributes are required:
UNNotificationExtensionCategory
A string value equal to the notification category which we created and set in the app delegate. This will let the content extension know which notification you want to have custom UI for.UNNotificationExtensionInitialContentSizeRatio
A number between 0 and 1 which determines the aspect ratio of your UI. The default value is 1 which will allow your interface to have its total height equal to its width.
I’ve also set UNNotificationExtensionDefaultContentHidden
to “YES” so that the default notification does not show when the content extension is running.
You can use the storyboard to set up your view or create the UI programmatically in the view controller. For this example I’ve set up my storyboard with an image view which will show the podcast logo, two labels for the title and body of the notification content, and a “Like” button which will show a heart image.
Now, in order to get the image showing for the podcast logo and the button, we need to go back to our notification request:
guard let pathUrlForPodcastImg = Bundle.main.url(forResource: "startup", withExtension: "jpg") else { return }
let imgAttachment = try! UNNotificationAttachment(identifier: "image", url: pathUrlForPodcastImg, options: nil)
guard let pathUrlForButtonNormal = Bundle.main.url(forResource: "heart-outline", withExtension: "png") else { return }
let buttonNormalStateImgAtt = try! UNNotificationAttachment(identifier: "button-normal-image", url: pathUrlForButtonNormal, options: nil)
guard let pathUrlForButtonHighlighted = Bundle.main.url(forResource: "heart-filled", withExtension: "png") else { return }
let buttonHighlightStateImgAtt = try! UNNotificationAttachment(identifier: "button-highlight-image", url: pathUrlForButtonHighlighted, options: nil)
content.attachments = [imgAttachment, buttonNormalStateImgAtt, buttonHighlightStateImgAtt]
I added a folder in my project that contains all the images we need for the notification so we can access them through the main bundle.
For each image, we get the file path and use that to create a UNNotificationAttachment
. Added that to our notification content allows us to access the images in the Notification Content Extension in the didReceive
method shown below.
func didReceive(_ notification: UNNotification) {
self.newEpisodeLabel.text = notification.request.content.title
self.episodeNameLabel.text = notification.request.content.body
let imgAttachment = notification.request.content.attachments[0]
let buttonNormalStateAtt = notification.request.content.attachments[1]
let buttonHighlightStateAtt = notification.request.content.attachments[2]
guard let imageData = NSData(contentsOf: imgAttachment.url), let buttonNormalStateImgData = NSData(contentsOf: buttonNormalStateAtt.url), let buttonHighlightStateImgData = NSData(contentsOf: buttonHighlightStateAtt.url) else { return }
let image = UIImage(data: imageData as Data)
let buttonNormalStateImg = UIImage(data: buttonNormalStateImgData as Data)?.withRenderingMode(.alwaysOriginal)
let buttonHighlightStateImg = UIImage(data: buttonHighlightStateImgData as Data)?.withRenderingMode(.alwaysOriginal)
imageView.image = image
likeButton.setImage(buttonNormalStateImg, for: .normal)
likeButton.setImage(buttonHighlightStateImg, for: .selected)
}
Now we can use the file path URLs we set in the request to grab the data for the URL and turn them into images. Notice that I have two different images for the different button states which will allow us to update the UI for user interaction. When I run the app and send the request, here’s what the notification looks like:
Everything I’ve mentioned so far in relation to the content extension isn’t new in iOS 12, so let’s dig into the two new features: User Interaction and Dynamic Actions. When the content extension was first added in iOS 10, there was no ability to capture user touch within a notification, but now we can register UIControl events and respond when the user interacts with a UI element.
For this example, we want to show the user that the “Like” button has been selected or unselected. We already set the images for the .normal
and .selected
states, so now we just need to add a target for the UIButton so we can update the selected state.
override func viewDidLoad() {
super.viewDidLoad()
// Do any required interface initialization here.
likeButton.addTarget(self, action: #selector(likeButtonTapped(sender:)), for: .touchUpInside)
}
@objc func likeButtonTapped(sender: UIButton) {
likeButton.isSelected = !sender.isSelected
}
Now with the above code we get the following behavior:
In the selector method likeButtonTapped
, we could also add any logic for saving the liked state in User Defaults or the Keychain, so we have access to it in our main application.
Notification actions have existed since iOS 10, but once you click on them, usually the user will be rerouted to the main application or the content extension is dismissed. Now in iOS 12, we can update the list of notification actions that are shown in response to which action the user selects.
First, let’s go back to our app delegate where we create our notification categories so we can add some actions to our podcast category.
let playAction = UNNotificationAction(identifier: "play-action", title: "Play", options: [])
let queueAction = UNNotificationAction(identifier: "queue-action", title: "Queue Next", options: [])
let podcastCategory = UNNotificationCategory(identifier: "podcast", actions: [playAction, queueAction], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenPreviewsPlaceholder, categorySummaryFormat: summaryFormat, options: [])
Now when we run the app and send a notification, we see the following actions shown below:
When the user selects “Play,” we want the action to be updated to “Pause.” If they select “Queue Next,” we want that action to be updated to “Remove from Queue.” We can do this in our didReceive
method in the Notification Content Extension’s view controller.
func didReceive(_ response: UNNotificationResponse, completionHandler completion:
(UNNotificationContentExtensionResponseOption) -> Void) {
guard let currentActions = extensionContext?.notificationActions else { return }
if response.actionIdentifier == "play-action" {
let pauseAction = UNNotificationAction(identifier: "pause-action", title: "Pause", options: [])
let otherAction = currentActions[1]
let newActions = [pauseAction, otherAction]
extensionContext?.notificationActions = newActions
} else if response.actionIdentifier == "queue-action" {
let removeAction = UNNotificationAction(identifier: "remove-action", title: "Remove from Queue", options: [])
let otherAction = currentActions[0]
let newActions = [otherAction, removeAction]
extensionContext?.notificationActions = newActions
} else if response.actionIdentifier == "pause-action" {
let playAction = UNNotificationAction(identifier: "play-action", title: "Play", options: [])
let otherAction = currentActions[1]
let newActions = [playAction, otherAction]
extensionContext?.notificationActions = newActions
} else if response.actionIdentifier == "remove-action" {
let queueAction = UNNotificationAction(identifier: "queue-action", title: "Queue Next", options: [])
let otherAction = currentActions[0]
let newActions = [otherAction, queueAction]
extensionContext?.notificationActions = newActions
}
completion(.doNotDismiss)
}
By resetting the extensionContext?.notificationActions
list to contain the updated actions, it allows us to change the actions every time the user selects one. The behavior is shown in the gif below.
Summary
There’s a lot to do before iOS 12 launches to make sure your notifications are ready. The steps vary in complexity and you don’t have to implement them all. Make sure to first download XCode 10 beta so you can try out the features we’ve gone over. If you want to play around with the demo app I’ve referenced throughout the article, check it out on Github.
For Your Notification Permissions Request And Settings, You’ll Need To:
- Determine whether or not you want to enable provisional authorization and add it to your authorization options.
- If you have already have a customized notification settings view in your app, add
providesAppNotificationSettings
to your authorization options as well as implement the call back in your app delegate or whichever class conforms toUNUserNotificationCenterDelegate
.
For Notification Grouping:
- Add a thread identifier to your remote and local notifications so your notifications are correctly grouped in the Notification Center.
- When registering your notification categories, add the category summary parameter if you want your grouped notification to be more descriptive than “more notifications.”
- If you want to customize the summary text even more, then add a summary identifier to match whichever formatting you added for the category summary.
For Customized Rich Notifications:
- Add the Notification Content extension target to your app to create rich notifications.
- Design and implement the view controller to contain whichever elements you want in your notification.
- Consider which interactive elements would be useful to you, i.e. buttons, table view, switches, etc.
- Update the
didReceive
method in the view controller to respond to selected actions and update the list of actions if necessary.
Further Reading
- “Notifications,” Apple’s list of various documentation regarding user notifications
- “What’s New in User Notifications,” WWDC 2018 Apple
- “Customizing the Appearance of Notifications,” Apple’s documentation on the Notification Content Extension