16

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.

gohnjanotis
  • 6,513
  • 6
  • 37
  • 57

2 Answers2

2

You can check this demo

I think you want to achieve the function is called "update notification".

iOS 10 allow update the notification.All you just do——keep the notifications have the same identifier.

Let's see a demo:

  1. first notification:

    NSURL * imageUrl = [[NSBundle mainBundle] URLForResource:@"dog" withExtension:@"png"];
    UNNotificationAttachment *imgAtt = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:imageUrl options:nil error:&error];
    
    NSURL * mp4Url = [[NSBundle mainBundle] URLForResource:@"media" withExtension:@"mp4"];
    UNNotificationAttachment *mediaAtt = [UNNotificationAttachment attachmentWithIdentifier:@"image" URL:mp4Url options:nil error:&error];
    
    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc]init];
    //在通知中心显示的总是第一个多媒体资源
    content.attachments = @[imgAtt,mediaAtt];
    content.badge = @1;
    content.title = @"Wake Up";
    content.subtitle = @"First time";
    content.body = @"next time。。。 ";
    content.categoryIdentifier = @"wakeup";
    content.launchImageName = @"dog";
    content.sound = [UNNotificationSound defaultSound];
//    content.threadIdentifier = @"";
    content.userInfo = @{@"first":@"5:00 am",@"second":@"6:00"};
    
    UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5.0 repeats:NO];
    
    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"com.junglesong.pushtestdemo.wakeup" content:content trigger:trigger];
    
    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
        NSLog(@"wake up message has been deliverd!");
    }];
  1. update the first notification:

    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc]init];
    content.badge = @1;
    content.title = @"Update!dear,wake up";
    content.subtitle = @"Update! dear,please";
    content.body = @"Update!shall we have breakfast?";
    content.categoryIdentifier = @"wakeup";
    content.launchImageName = @"dog";
    content.sound = [UNNotificationSound defaultSound];
    //    content.threadIdentifier = @"";
    content.userInfo = @{@"first":@"5:00 am",@"second":@"6:00"};
    
    UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:5.0 repeats:NO];
    
    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"com.junglesong.pushtestdemo.wakeup" content:content trigger:trigger];
    
    [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
        NSLog(@"wake up message has been updated!");
    }];

Now, you add two notification.But the system treats them as the same.So there is just only one.And the sencond replaces the first one.In iOS 10,call this update.

The property "identifier" is the id of UNNotificationRequest,which can differentiate notifications.

wenghengcong
  • 569
  • 5
  • 11
  • 3
    It may seem like this could be a solution, however if I try to use the same identifier in this way I only end up with one notification scheduled. It only has the information of the last update and only shows once rather than multiple notification updates scheduled to show an updated notification every time a new one is supposed to be delivered. – gohnjanotis Sep 27 '16 at 07:50
0

You can just use removeDeliveredNotifications(withIdentifiers:)

Owen Zhao
  • 3,205
  • 1
  • 26
  • 43
  • 3
    That is the correct function for clearing notifications, but the problem is triggering it. The issue here is that there is no method that gets called when a non-remote notification is delivered and the app is not in the foreground, so there's nowhere to call that when a notification is delivered to clear old already-delivered notifications. – gohnjanotis Aug 20 '16 at 14:20
  • Find a solution yet? Same issue. – Justin Apr 17 '17 at 19:14
  • @gohnjanotis not sure if this would be helpful, but see my answer [here](https://stackoverflow.com/questions/44363767/assure-delivery-of-uilocalnotification-on-currect-time-ios/44364338#44364338) – mfaani Jun 05 '17 at 09:00
  • @Justin I have not found a solution. I don't think there is a way to do what I want to with the current APIs. – gohnjanotis Jun 05 '17 at 13:30
  • @Honey If you're suggesting implementing the notification clearing code in userNotificationCenter(_:didReceive:withCompletionHandler:), that will not get the desired results, since that function is only called when a user performs an action on a notification. – gohnjanotis Jun 05 '17 at 14:14