This question is about the new UserNotifications
framework in iOS 10.
I have an app that schedules a local notification every half hour after the user did a certain action in the app, starting at 1 hour.
To avoid cluttering a user's lock screen or notification center, I only want one notification to show up at a time, so there is only one notification with the most recent, relevant information. My plan to achieve this is to clear all delivered notifications any time a new notification is presented.
It seems this should be possible with the new willPresent
method of UNUserNotificationCenterDelegate
, but it's not behaving as I would expect.
Here's a function that I call to set up all the notifications, starting at 1 hour after the event in the app, and scheduling a notification every half hour until the last one at 23.5 hours after the event:
func updateNotifications() {
for hour in 1...23 {
scheduleNotification(withOffsetInHours: Double(hour))
scheduleNotification(withOffsetInHours: Double(hour) + 0.5)
}
}
This is the function that actually schedules the notifications based on the mostRecentEventDate
, which is a Date
set elsewhere:
func scheduleNotification(withOffsetInHours: Double) {
// set up a Date for when the notification should fire
let offsetInSeconds = 60 * 60 * withOffsetInHours
let offsetFireDate = mostRecentEventDate.addingTimeInterval(offsetInSeconds)
// set up the content of the notification
let content = UNMutableNotificationContent()
content.categoryIdentifier = "reminder"
content.sound = UNNotificationSound.default()
content.title = "Attention!"
content.body = "It has been \(withOffsetInHours) hours since the most recent event."
// set up the trigger
let triggerDateComponents = Calendar.current.components([.year, .month, .day, .hour, .minute, .second], from: offsetFireDate)
let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDateComponents, repeats: false)
// set up the request
let identifier = "reminder\(withOffsetInHours)"
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
// add the request for this notification
UNUserNotificationCenter.current().add(request, withCompletionHandler: { (error) in
if error != nil {
print(error)
}
})
}
In my UNUserNotificationCenterDelegate
I have the willPresent
method set up like this:
func userNotificationCenter(_: UNUserNotificationCenter, willPresent: UNNotification, withCompletionHandler: (UNNotificationPresentationOptions) -> Void) {
print("will present...")
UNUserNotificationCenter.current().removeAllDeliveredNotifications()
withCompletionHandler([.alert,.sound])
}
I know the willPresent
function is being called because it prints "will present..." but the existing notifications in the notification center don't get cleared. Does anyone know why this doesn't work? Or if there's a way to get it working the way I want it to?
EDIT: I came up with an alternate approach to achieve the same thing, but it also doesn't seem to work.
My idea here was to use willPresent
to silence the incoming scheduled notification while scheduling another notification to arrive immediately (without a trigger). All the notifications being scheduled to arrive immediately have the same identifier, so the existing notification with that identifier should always be replaced, like in the example around 20:00 in this 2016 WWDC talk about the new UserNotifications framework. Here's my updated willPresent
method:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent: UNNotification, withCompletionHandler: (UNNotificationPresentationOptions) -> Void) {
let identifier = willPresent.request.identifier
if identifier != "reminder" {
let offsetInHoursString = identifier.replacingOccurrences(of: "reminder", with: "")
let content = UNMutableNotificationContent()
content.categoryIdentifier = "reminder"
content.sound = UNNotificationSound.default()
content.title = "Attention!"
content.body = "It has been \(offsetInHoursString) hours since the most recent event."
let identifier = "hydrationReminder"
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
center.add(request, withCompletionHandler: { (error) in
if error != nil {
print(error)
}
})
withCompletionHandler([])
} else {
withCompletionHandler([.alert,.sound])
}
}
EDIT: I finally realized that willPresent
will only be called if the app is in the foreground, as it says right at the top of this page, so neither of these approaches should actually work. I thought willPresent
would be called every time a notification was received. Back to the drawing board on this "only the newest, most relevant notification" idea...
UPDATE (July 2018): With the introduction of grouped notifications in iOS 12, updating an older notification (with the goal of reducing notification clutter) seems less relevant. I'd still like to be able to do it to minimize the appearance of clutter, and it seems there should be feature parity between remote push notifications, which can be updated after the fact, and local notifications, which can not be updated later. However, since Apple has introduced grouped notifications, I'd expect its less likely they'd implement the ability to update old local notifications in favor of letting apps just send new ones and having them group together with existing notifications.