MacKuba

Kuba Suder's blog on iOS & Mac development

What's new in notifications in iOS 12

Categories: Cocoa, iPhone 2 comments

One of the things that caught my attention in the WWDC videos I watched last week were the improvements in the notification system, meant to on one hand give more control over notifications to the users and make them a bit less overwhelming, and on the other hand make them more useful by allowing them to be more interactive.

I’ve tried to sum up here all the changes related to notifications that I’ve found – there’s nothing here that you can’t find by yourself in the videos, but I figured it’s worth putting it all in one place in a concise form.

Most of the below applies to both iOS 12 and watchOS 5.

Notification grouping

One feature that users will probably notice very quickly is that notifications are now grouped on the lock screen and in the notification center. This happens automatically, but you (and your users) will be able to customize it. The user can expand and collapse the whole group to see and interact with specific notifications in it, and they can clear specific notifications or the whole group.

A stack of notifications that says: Such App - Woow - Much content - 3 more notifications 3 expanded notifications from Such App: Woow, Much content / OMG, Very app / Wow, So notifying

By default, notifications will be grouped by app – which means all notifications from your app will be packed into one stack, showing only the newest one (called “leading” in the videos) in detail, with a small subtitle like “5 more notifications” below. This can make sense for a lot of apps, if they don’t have any concept of threads/conversations or any other way of dividing notifications into distinct groups. But if your app does, you can customize this.

To override the default grouping, you can assign specific notifications their “thread IDs”. You might already be doing this, since it’s not a new feature – this was added last year in order to let you group notifications with hidden content on a locked device’s lock screen. The whole grouping API added now is really just an extension of that system from iOS 11, so if you’ve implemented hidden previews, this works in a very similar way.

So if you do assign a thread ID, all notifications for a given thread will be grouped together, but separately from other threads. Some examples of how this might be used showed in the video:

  • in the Messages app, messages are grouped by conversation
  • in Mail they’re grouped by account by default, then conversations with VIP contacts are grouped separately by person, and threads manually followed are grouped by thread
  • Calendar app groups most notifications in one group (thread ID = nil), except important notifications about events starting soon that are separate from the rest

General tips mentioned there included:

Separate important, actionable notifications from informative updates

Create groups for meaningful, personal communication

Respect the user’s priorities and organization

Also, take into account that the user can override this to still put all your notifications into one group, or to not group them at all. And you probably shouldn’t try to abuse this system to put every notification in a new thread just because…

If the user opens a Notification Content Extension from a notification group, the view only gets the newest notification, but it can use getDeliveredNotifications to load the rest of them (in case you want to e.g. show a complete thread of received messages). Any incoming notifications from that thread received while the view is open will also be passed to the extension’s didReceive(_:) so they can be appended to the displayed list. It’s recommended to also clean up notifications displayed in such view from the notification center by calling removeDeliveredNotifications(withIdentifiers:).

Customizing group summary

One thing you can further customize is the summary below grouped content. By default, it says “x more notifications”, but you can change it to say e.g. “5 more messages” or “8 more episodes”. You do this by defining a UNNotificationCategory (you need to have one) and passing a format string like "%u more messages from %@" to the categorySummaryFormat argument. Two forms are allowed: either with one %u, or with one %u and one %@.

If you do include %@ (to show something like “from John” or “from GMail”), pass either a summary-arg field in the JSON payload for remote notifications or summaryArgument property for local notifications to fill that space. There’s also a second parameter, summary-arg-count or summaryArgumentCount respectively, that allows you to have one notification count as more than 1 when calculating the sum. This is useful when a single notification’s text says something like “Kate uploaded 2 pictures” – in that case, the summaryArgumentCount parameter lets you set the “weight” of a given notification.

Woow - Much content - 3 more messages from Doge

iOS tries to combine these summaries when possible:

  • summaries of notifications of the same category, but from different sources, are combined to e.g. “10 more pictures from Kate and John”
  • summaries from different categories that only specify a count (%u) but no source (%@) are combined to e.g. “5 more messages and 4 more pictures”
  • summaries from different categories where some of them include a %@ are combined to the default “x more notifications” message

And of course you should probably think about localizing all of this to multiple languages… In this case, look into NSString’s method localizedUserNotificationString(forKey:arguments:), introduced two years ago with UserNotifications, and the stringsdict file format used for defining localized labels with different variants of plurals.

Quiet and prominent notifications

iOS 12 introduces a concept of “quiet notifications”. Quiet notifications are notifications which will only appear in the notification center, but they don’t display an alert, they don’t appear on the lock screen and they don’t make any sounds (they are allowed to set a badge). This is meant to make the user’s lock screen less crowded and the notifications less distracting. For some less essential (or more spammy) apps, it may make sense to only see their notifications from time to time when you check the notification center, but not be attacked with them on the lock screen and with banners while you’re trying to do something more productive.

The user can switch an app’s notifications into quiet mode in a few ways:

  • slide a notification on the list and tap the “Manage” button
  • tap the “…” button in the notification content view
  • iOS will also periodically suggest that you might want to limit some app’s notifications if you don’t seem to react to them at all

All of these lead to the panel shown below, in which the user can either turn off given app’s notifications completely, or switch them to the quiet mode:

Such App - Manage notifications: Deliver Quietly / Turn Off... / Settings / Cancel

From what I can see, this seems to simply turn off the “Lock Screen” and “Banners” options in the notification settings, so the user can switch it back at any point if they want to:

In the settings app: Allow Notifications | Alerts: Lock Screen (off) / Notification Center (on) / Banners (off)

They can also access the same “Manage” panel when notifications are set to quiet, in which case they have an option to “Deliver Prominently” instead.

Provisional authorization

If you think this kind of notification delivery is enough for your app, you can start in the quiet mode from the beginning – and as a bonus, you don’t have to ask the user for permission to deliver notifications at all (!). This is meant as a kind of “trial” mode of notifications, that this way the user can see if your notifications are worth seeing, but they don’t have to agree to let you fill their lock screen with them before they see what you’re actually going to send them.

If you want to take this path, pass the .provisional key when calling requestAuthorization(), and the permission popup should not appear:

let notificationCenter = UNUserNotificationCenter.current()

notificationCenter.requestAuthorization(options: [.alert, .badge, .provisional]) { ... }

Notification settings link

For some apps, it might make sense to only disable some kinds of notifications and leave others – kind of like most social networks let you customize which kinds of notification emails you want to receive from them. So Apple has added another feature in the API, which is meant to give you a chance to “save” your app from having its notifications turned off completely.

The way it’s done is that you implement a userNotificationCenter:openSettingsFor: callback in your app, and when it’s called, you show a screen inside your app that lets the user tweak which notifications to receive, where they can turn on and off specific categories of available notifications. You also need to pass an additional key .providesAppNotificationSettings when calling requestAuthorization() (I don’t think this was mentioned, but it doesn’t seem to work without that).

If you implement this, a link to this screen will be shown in two places in the system:

  • in the “Manage” popup – only when the user tries to turn off all notifications for your app
  • on your app’s notification settings page in the Settings app

Are you sure you want to turn off all notifications from Such App? - Turn off all notifications / Configure in Such App… In the settings app: Show Previews / Notification Grouping / Such App Notification Settings

Interactive notifications

The UserNotificationsUI API has also been updated to allow a lot more interactivity in the Notification Content Extension. There are two main changes here:

The first one is that the list of action buttons presented below the content doesn’t have to be static anymore. Previously you had to define a list of buttons for each category of notifications when defining UNNotificationCategory and passing a list of them to UNUserNotificationCenter’s setNotificationCategories. However, once you got to the extension view, it would always display what was configured earlier in the main app.

Now, inside the extension you can set the NSExtensionContext’s notificationActions property to update the list of action buttons. This can be done when the view appears, to make the selection of buttons depend on what kind of notification is received, or in reaction to the user’s interaction with the view (example: replace a “Like” button with “Unlike” after the user performs a like action).

The second change is that the content area itself can now accept user interaction. You need to opt in to this by setting a UNNotificationExtensionUserInteractionEnabled property under NSExtensionAttributes in your Info.plist. This lets you render whatever you want in that content box, some kind of custom UI with buttons that the user might tap etc.

You also have two new methods available on NSExtensionContext that you might want to call from your custom content UI:

  • performNotificationDefaultAction launches your main app (in non-interactive content extensions this happens when the user taps the content area, but now any tap events will be forwarded to your view) – this calls the action handling callback userNotificationCenter(_:didReceive:withCompletionHandler:) with action identifier UNNotificationDefaultActionIdentifier
  • dismissNotificationContextExtension simply hides the notification view as if the user closed it

Critical alerts

While most of the work this year went into basically fighting back against the deluge of notifications and letting users see less of them, Apple has realized that this could have unintended bad consequences in some rare scenarios. There are some kinds of notifications that you normally get rarely, if ever, but when you do get them, it’s really, really important that you do not miss them, and ideally see them immediately. The example given was a notification about blood glucose level for a diabetic person.

So apart from limiting users' exposure to notifications, Apple is adding one special kind of notification that goes the other way: it ignores your Do Not Disturb settings, and it even ignores the physical mute switch in your iPhone (!).

Such notification is displayed on the lock screen with a special red warning icon, and it not only can, but (if I understand correctly) has to include a sound – either a default sound for critical alerts (not sure what that is, but probably something you don’t want to play at 2am by accident), or one selected by you.

In order to send such notification, you need to ask for notifications permission with an additional .criticalAlert key:

let notificationCenter = UNUserNotificationCenter.current()

notificationCenter.requestAuthorization(options: [.sound, .badge, .alert, .criticalAlert]) { ... }

The user will get a special kind of permission popup with a red alert icon, explaining what critical alerts are. They of course have a right to refuse granting this permission, and they can also take it back later at any moment.

To actually send a critical alert, you send a notification as usual, except:

  • for remote notifications, add a "critical": 1 property to the JSON payload
  • for local notifications, you need to use a specially configured sound:
let content = UNMutableNotificationContent()
// ...

// system sound:
content.sound = UNNotificationSound.defaultCritical

// or custom sound:
content.sound = UNNotificationSound.criticalSoundNamed("fire-alarm")

You can also override the notification volume that the user has set in their settings (in the range from 0 to 1):

// for system sound:
content.sound = UNNotificationSound.defaultCriticalSound(withAudioVolume: 1.0)

// for custom sound:
content.sound = UNNotificationSound.criticalSoundNamed("fire-alarm", withAudioVolume: 1.0)

Of course this feature is something that some developers would want to abuse, so to be able to send these critical alerts, your apps needs to include a special custom entitlement that you will only get from Apple on request. You need to apply through this form and explain what kind of app your building and what you need critical alerts for. And you probably need to have a really good reason to convince them.

The kinds of apps that Apple mentioned this feature might make sense for are for example:

  • medical and health-related apps
  • home security
  • public safety (e.g. warnings about severe weather forecast)

In general, this should be something that requires the user to take some kind of action immediately (in real life), where otherwise their or others' health or life might be at risk if they don’t.

User Notifications on watchOS

All of the above is also available on watchOS, specifically:

  • notification grouping
  • an option for users to silence or disable notifications from the notification center
  • provisional quiet notifications
  • interactive notification content (turn on “Has Interactive Interface” on the storyboard scene)
  • performNotificationDefaultAction() and performDismissAction() in WKUserNotificationInterfaceController
  • updating notificationActions in WKUserNotificationInterfaceController, in didReceive() or later
  • handling new incoming notifications in the notification extension (turn on “Handles Grouping”)
  • critical alerts

User Notifications on macOS

As far as I can tell, macOS didn’t get any of the above features – but they’re probably not needed as much there. However, it did finally get access to the UserNotifications framework itself which for some reason it didn’t get 2 years ago, unlike every other Apple platform. This means you now get access to a more modern API that unifies handling of local and remote notifications and adds various features known from the iOS version.

Registering for notifications is now done using registerForRemoteNotifications() (no parameters), coupled with requestAuthorization(...) on UNUserNotificationCenter, known from the other platforms. The old NSUserNotification API that has served us since oh-es-ex 10.8 and has replaced the previous de facto standard Growl (RIP) is now soft-deprecated (Xcode and docs don’t report it as deprecated, but the comments say it is), as is registerForRemoteNotifications(matching: ...).


Links to relevant WWDC videos:

2 comments:

Chris

Hi! Great blog post! Wondering if you could help answer a question with regard to provisional authorization. Let's say a user on my app is on iOS 11, and is currently opted out of (turned off) notifications. Then, in my next app release I add support for provisional authorization. When that user upgrades to iOS 12 (and I guess updates my app), will I then be able to automatically start sending them quiet notifications? Will the notification settings automatically change to on + quiet? Or will their opted-out setting be kept? It's been hard to find an answer to this short of actually testing it out. Thanks!

Kuba

@Chris: huh, good question... but I guess you'll have to ask someone to test it. But my common sense thinking would be that if someone was asked about notifications permission and said no, or went to the settings later and disabled them manually, then that's a way of explicitly saying "no, I don't want notifications from this app", so this setting should be unchanged after the update and you still wouldn't be able to send notifications.

As I understand, provisional notifications are a way for the user to see if your app's notifications are interesting before they decide if they want to allow them on their lock screen etc., but in this case it seems they've already made a decision, for whatever reason.

Leave a comment

*

*
This will only be used to display your Gravatar image.

What operating system runs on Macs? *