开发者

iPhone: Incrementing the application badge through a local notification

is it possible to increment the application badge through a local notification while the app is not running?

I know how to set the badge, but haven't found any way to increment this value.

localNotification.applicationIconBadgeNumber = 23;

Update: I found a (far from being perfect) solution. You can predict what will happen, if the user doesn't open the app and add notifications for every +1 event.

An example:

  • For day 1: Count = 0
  • For day 2: localNotification.applicationIconBadgeNumber = 1;
  • For day 3: localN开发者_如何学JAVAotification.applicationIconBadgeNumber = 2;
  • For day 4: localNotification.applicationIconBadgeNumber = 3;

==> Put these notifications in an array and set them before the application exits.

However, I'm searching for a better solution than this workaround.


I've found, implemented & tested a 'workaround' for (apparantly) auto-incrementing the app icon's badge number, that works fine with non-repeating local notifications

It is indeed not possible for UILocalNotifications to have iOS 'automatically' update/increment the badge number when multiple local notifications are fired, and the user 'ignores' them or doesn't handle them immediately, so they 'pile up' in the Notification centre.

Also 'adding some callback method' to your app cannot take care of the 'auto increment', because the whole notification thing is handled 'outside' of your app by iOS, your app doesn't even need to be running.

However there is some workaround, that is based on the knowledge which I found through experimenting, because the XCode documentation is too vague on the badge property.

  • the badge is just an 'integer', actually more like a 'dummy label' that you assign to the applicationIconBadgeNumber property, right before you register the notification. You can give it any value - when the notification fires, iOS will add that value to the badge, whatever you set it to at the time you registered the notification. There is no magic 'auto-increment' or other manipulation by iOS (maybe that is different with push notifications, but that's not the subject here). iOS just takes the number (integer) from the registered notification, and puts it in the badge.

So for a 'workaround' your app must already provide the correct, incrementing badge number for each notification it newly creates and registers 'on top of the pending notifications'.

Since your app cannot look in the future, and know which events you'll handle immediately, and which ones you'll leave 'pending' for a while, there's some trick to do :

When notifications are handled by your app (by tapping on the notification(s), icon, ...), you have to :

  1. get a copy of all pending notifications
  2. 'renumber' the badge number of these pending notifications
  3. delete all pending notifications
  4. re-register the copy of the notifications with their corrected badge numbers again

Also, when your app registers a new notification, it has to check how many notifications are pending first, and register the new notification with with :

badgeNbr = nbrOfPendingNotifications + 1;

Looking at my code, it will get clearer. I tested this, and it's definitely working :

In your 'registerLocalNotification' method you should do this :

NSUInteger nextBadgeNumber = [[[UIApplication sharedApplication] scheduledLocalNotifications] count] + 1;
localNotification.applicationIconBadgeNumber = nextBadgeNumber;

When you handle the notification (appDelegate), you should call the method below, which clears the badge on the icon and renumbers the badges for pending notifications (if there are any)

Note that the next code works fine for 'sequential' registered events. If you would 'add' events in between pending ones, you'll have to 're-sort' these events first. I didn't go that far, but I think it's possible.

- (void)renumberBadgesOfPendingNotifications
{
    // clear the badge on the icon
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];

    // first get a copy of all pending notifications (unfortunately you cannot 'modify' a pending notification)
    NSArray *pendingNotifications = [[UIApplication sharedApplication] scheduledLocalNotifications];

    // if there are any pending notifications -> adjust their badge number
    if (pendingNotifications.count != 0)
    {
        // clear all pending notifications
        [[UIApplication sharedApplication] cancelAllLocalNotifications];

        // the for loop will 'restore' the pending notifications, but with corrected badge numbers
        // note : a more advanced method could 'sort' the notifications first !!!
        NSUInteger badgeNbr = 1;

        for (UILocalNotification *notification in pendingNotifications)
        {
            // modify the badgeNumber
            notification.applicationIconBadgeNumber = badgeNbr++;

            // schedule 'again'
            [[UIApplication sharedApplication] scheduleLocalNotification:notification];
        }
    }
}

To be truly 'bullet proof', this method should be 'atomic' (kernel) code, preventing the iOS from firing of a notification during the execution of this method. We'll have to take this risk here, chances are very small this will happen.

This is my first contribution to Stackoverflow, so you can comment also if I'm not following the 'rules' here


The only way you're going to be able to dynamically set the badge number when your application isn't running is with push notifications. You'll have to track the updates on the server side.


Based on the documentation , I believe you can not increment the value of the badge, when your application is not running. You set the badge number when you schedule your notification, so it is not possible to increment it.

An application is responsible for managing the badge number displayed on its icon. For example, if a text-messaging application processes all incoming messages after receiving a local notification, it should remove the icon badge by setting the applicationIconBadgeNumber property of the UIApplication object to 0.


since iOS10 it's possible to define the badge number directly on the UNMutableNotificationContent.

Here what's works for me :

I'm working on an application which adds Notification based on a Date (with CalendarComponents), my trigger is UNCalendarNotificationTrigger. My code is simply:

let content = UNMutableNotificationContent()
        content.title = "Title"
        content.body = "Your message"
        content.sound = .default()
        content.badge = NSNumber(value: UIApplication.shared.applicationIconBadgeNumber + 1)

About content.badge, the doc says :

var badge: NSNumber? { get set }

Description The number to apply to the app’s icon.

Use this property to specify the number to apply to the app’s icon when the notification arrives. If your app is not authorized to display badge-based notifications, this property is ignored.

Specify the number 0 to remove the current badge, if present. Specify a number greater than 0 to display a badge with that number. Specify nil to leave the current badge unchanged.

SDKs iOS 10.0+, tvOS 10.0+, watchOS 3.0+

The badge increments itself when a notification is added even if the app is not running. You can clear the badge number wherever you want in the app with :

UIApplication.shared.applicationIconBadgeNumber = 0


Add following code in your project delegate.

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    NSLog(@"%s",__FUNCTION__);

    NSArray *arrayOfLocalNotifications = [[UIApplication sharedApplication] scheduledLocalNotifications] ;

    for (UILocalNotification *localNotification in arrayOfLocalNotifications) {
        NSLog(@"the notification: %@", localNotification);
        localNotification.applicationIconBadgeNumber= application.applicationIconBadgeNumber+1;
    }
}

this works for me. :-)


Whasssaabhhh's answer in Swift 2.1, with sorting

func renumberBadgesOfPendingNotifications() {
    let app = UIApplication.sharedApplication()
    let pendingNotifications = app.scheduledLocalNotifications

    // clear the badge on the icon
    app.applicationIconBadgeNumber = 0

    // first get a copy of all pending notifications (unfortunately you cannot 'modify' a pending notification)
    // if there are any pending notifications -> adjust their badge number
    if let pendings = pendingNotifications where pendings.count > 0 {

        // sorted by fire date.
        let notifications = pendings.sort({ p1, p2 in p1.fireDate!.compare(p2.fireDate!) == .OrderedAscending })

        // clear all pending notifications
        app.cancelAllLocalNotifications()

        // the for loop will 'restore' the pending notifications, but with corrected badge numbers
        var badgeNumber = 1
        for n in notifications {

            // modify the badgeNumber
            n.applicationIconBadgeNumber = badgeNumber++

            // schedule 'again'
            app.scheduleLocalNotification(n)
        }
    }
}


Whasssaaahhh's answer was very helpful to me. I also needed to sort the notifications based on their fireDates. Here's Whasssaaahhh's code with my code to sort the notifications using the NSArray's delegate method for sorting - [NSArray sortedArrayUsingComparator:^(id obj1, id obj2) {}];

- (void)renumberBadgesOfPendingNotifications
{
    // clear the badge on the icon
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];

    // first get a copy of all pending notifications (unfortunately you cannot 'modify' a pending notification)
    // Sort the pending notifications first by their fireDate
    NSArray *pendingNotifications = [[[UIApplication sharedApplication] scheduledLocalNotifications] sortedArrayUsingComparator:^(id obj1, id obj2) {
        if ([obj1 isKindOfClass:[UILocalNotification class]] && [obj2 isKindOfClass:[UILocalNotification class]])
        {
            UILocalNotification *notif1 = (UILocalNotification *)obj1;
            UILocalNotification *notif2 = (UILocalNotification *)obj2;
            return [notif1.fireDate compare:notif2.fireDate];
        }

        return NSOrderedSame;
    }];

    // if there are any pending notifications -> adjust their badge number
    if (pendingNotifications.count != 0)
    {
        // clear all pending notifications
        [[UIApplication sharedApplication] cancelAllLocalNotifications];

        // the for loop will 'restore' the pending notifications, but with corrected badge numbers
        // note : a more advanced method could 'sort' the notifications first !!!
        NSUInteger badgeNbr = 1;

        for (UILocalNotification *notification in pendingNotifications)
        {
            // modify the badgeNumber
            notification.applicationIconBadgeNumber = badgeNbr++;

            // schedule 'again'
            [[UIApplication sharedApplication] scheduleLocalNotification:notification];
        }
    }
}

After sometime, I needed to implement this on Swift but also needed to support repeating local notifications. I've come up with a solution on Swift.

Solution for Swift 2.3

func renumberBadgesOfPendingNotifications() {
    let app = UIApplication.sharedApplication()
    let pendingNotifications = app.scheduledLocalNotifications

    // clear the badge on the icon
    app.applicationIconBadgeNumber = 0

    // first get a copy of all pending notifications (unfortunately you cannot 'modify' a pending notification)
    // if there are any pending notifications -> adjust their badge number
    if let pendings = pendingNotifications where pendings.count > 0 {

        // Reassign firedate.
        var notifications = pendings
        var i = 0
        for notif in notifications {
            if notif.fireDate?.compare(NSDate()) == NSComparisonResult.OrderedAscending &&
            notif.repeatInterval.rawValue == NSCalendarUnit.init(rawValue:0).rawValue {
                // Skip notification scheduled earlier than current date time
                // and if it is has NO REPEAT INTERVAL
            }
            else {
                notif.fireDate = getFireDate(notif)
            }

            i+=1
        }

        // sorted by fire date.
        notifications = pendings.sort({ p1, p2 in p1.fireDate!.compare(p2.fireDate!) == .OrderedAscending })

        // clear all pending notifications
        app.cancelAllLocalNotifications()

        // the for loop will 'restore' the pending notifications, but with corrected badge numbers
        var badgeNumber: Int = 1
        for n in notifications {
            // modify the badgeNumber
            n.applicationIconBadgeNumber = badgeNumber

            badgeNumber+=1
            // schedule 'again'
            app.scheduleLocalNotification(n)
        }
    }
}

private func getFireDate(notification:UILocalNotification?) -> NSDate? {
        if notification == nil {
            return nil
        }

        let currentDate: NSDate = NSDate().dateByRemovingSeconds()
        let originalDate: NSDate = notification!.fireDate!
        var fireDate: NSDate? = originalDate

        if originalDate.compare(currentDate) == NSComparisonResult.OrderedAscending ||
            originalDate.compare(currentDate) == NSComparisonResult.OrderedSame {

            let currentDateTimeInterval = currentDate.timeIntervalSinceReferenceDate
            let originalDateTimeInterval = originalDate.timeIntervalSinceReferenceDate
            var frequency:NSTimeInterval = 0

            switch notification?.repeatInterval {
            case NSCalendarUnit.Hour?:
                frequency = currentDate.dateByAddingHours(1).timeIntervalSinceDate(currentDate)
                print(frequency)
                break
            case NSCalendarUnit.Day?:
                frequency = currentDate.dateByAddingDays(1).timeIntervalSinceDate(currentDate)
                print(frequency)
                break
            case NSCalendarUnit.WeekOfYear?:
                frequency = currentDate.dateByAddingDays(7).timeIntervalSinceDate(currentDate)
                print(frequency)
                break
            case NSCalendarUnit.Month?:
                frequency = currentDate.dateByAddingMonths(1).timeIntervalSinceDate(currentDate)
                print(frequency)
                break
            case NSCalendarUnit.Year?:
                frequency = currentDate.dateByAddingYears(1).timeIntervalSinceDate(currentDate)
                print(frequency)
                break
            default:
                originalDate
            }

            let timeIntervalDiff = (((currentDateTimeInterval - originalDateTimeInterval) / frequency) + frequency) + originalDateTimeInterval
            fireDate = NSDate(timeIntervalSinceReferenceDate: timeIntervalDiff)
        }

        return fireDate?.dateByRemovingSeconds()
    }

Note: dateByAddingHours, dateByAddingHours, dateByAddingMonths, dateByAddingYears, dateByRemovingSeconds are methods from a DateExtension I'm using and are self-descriptive methods you can implement on your own.


As an alternative to Bionicle's solution, one can use a NSSortDescriptor to handle sorting based on the fireDate field. Again this solution provides all the benefits of Whasssaaahhh's original answer, but also means it can handle notifications being added in non chronological order, e.g. adding a notification in 30 seconds time, then in 20 seconds time. I call the below function when adding a local notification, and when returning to the application.

// When we add/remove local notifications, if we call this function, it will ensure each notification
// will have an ascending badge number specified.
- (void)renumberBadgesOfPendingNotifications
{
    // Clear the badge on the icon
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];

    // First get a copy of all pending notifications (unfortunately you cannot 'modify' a pending notification)
    NSMutableArray * pendingNotifications = [[[UIApplication sharedApplication] scheduledLocalNotifications] mutableCopy];

    // Sorted by fire date.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"fireDate" ascending:TRUE];
    [pendingNotifications sortUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];
    [sortDescriptor release];

    // if there are any pending notifications -> adjust their badge number
    if (pendingNotifications.count != 0)
    {
        // clear all pending notifications
        [[UIApplication sharedApplication] cancelAllLocalNotifications];

        // the for loop will 'restore' the pending notifications, but with corrected badge numbers
        // note : a more advanced method could 'sort' the notifications first !!!
        NSUInteger badgeNbr = 1;

        for (UILocalNotification *notification in pendingNotifications)
        {
            // modify the badgeNumber
            notification.applicationIconBadgeNumber = badgeNbr++;

            // schedule 'again'
            [[UIApplication sharedApplication] scheduleLocalNotification:notification];
        }
    }

    // Release our copy.
    [pendingNotifications release];
}


Based on Wassaahbbs and Bionicles answers above, for Swift 3.0 this seems to be working for Repeating Local Notifications. I have it working for setting 4 local notifications, each of which can be turned on and off independently.

The renumberBadgesOfPendingNotifications function is called in AppDelegate applicationDidBecomeActive so badges are updated if user opens the app after being notified. And also in a settingsVC where a setNotification function sets the notifications in the first place and In case user turns a notification on or off thus needing a badge update.

Also the badge is set to 0 in applicationDidBecomeActive with UIApplication.shared.applicationIconBadgeNumber = 0.

func renumberBadgesOfPendingNotifications() {
    // first get a copy of all pending notifications (unfortunately you cannot 'modify' a pending notification)
    let pendingNotifications = UIApplication.shared.scheduledLocalNotifications
    print("AppDel there are \(pendingNotifications?.count) pending notifs now")

    // if there are any pending notifications -> adjust their badge number
    if var pendings = pendingNotifications, pendings.count > 0 {

        // sort into earlier and later pendings
        var notifications = pendings
        var earlierNotifs = [UILocalNotification]()
        var laterNotifs = [UILocalNotification]()

        for pending in pendings {

            // Skip notification scheduled earlier than current date time
            if pending.fireDate?.compare(NSDate() as Date) == ComparisonResult.orderedAscending {
                // and use this if it has NO REPEAT INTERVAL && notif.repeatInterval.rawValue == NSCalendar.Unit.init(rawValue:0).rawValue {

                // track earlier and later pendings
                earlierNotifs.append(pending)
            }
            else {
                laterNotifs.append(pending)
            }
        }

        print("AppDel there are \(earlierNotifs.count) earlier notifications")
        print("AppDel there are \(laterNotifs.count) later notifications")

        // change the badge on the notifications due later
        pendings = laterNotifs

        // sorted by fireDate.
        notifications = pendings.sorted(by: { p1, p2 in p1.fireDate!.compare(p2.fireDate!) == .orderedAscending })

        // clear all pending notifications. i.e the laterNotifs
        for pending in pendings {
            UIApplication.shared.cancelLocalNotification(pending)
        }

        // the for loop will 'restore' the pending notifications, but with corrected badge numbers
        var laterBadgeNumber = 0
        for n in notifications {

            // modify the badgeNumber
            laterBadgeNumber += 1
            n.applicationIconBadgeNumber = laterBadgeNumber

            // schedule 'again'
            UIApplication.shared.scheduleLocalNotification(n)
            print("AppDel later notif scheduled with badgenumber \(n.applicationIconBadgeNumber)")
        }

        // change the badge on the notifications due earlier
        pendings = earlierNotifs

        // sorted by fireDate.
        notifications = pendings.sorted(by: { p1, p2 in p1.fireDate!.compare(p2.fireDate!) == .orderedAscending })

        // clear all pending notifications. i.e the laterNotifs
        for pending in pendings {
            UIApplication.shared.cancelLocalNotification(pending)
        }

        // the for loop will 'restore' the pending notifications, but with corrected badge numbers
        var earlierBadgeNumber = laterBadgeNumber
        for n in notifications {

            // modify the badgeNumber
            earlierBadgeNumber += 1
            n.applicationIconBadgeNumber = earlierBadgeNumber

            // schedule 'again'
            UIApplication.shared.scheduleLocalNotification(n)
            print("AppDel earlier notif scheduled with badgenumber \(n.applicationIconBadgeNumber)")
        }
    }
}


Based on Wassaahbbs and Bionicles answers above. Swift 4.0, for all iOS versions. Call this function in func applicationDidBecomeActive(_ application: UIApplication).

func renumberBadgesOfPendingNotifications() {
    if #available(iOS 10.0, *) {
        UNUserNotificationCenter.current().getPendingNotificationRequests { pendingNotificationRequests in
            if pendingNotificationRequests.count > 0 {
                let notificationRequests = pendingNotificationRequests
                    .filter { $0.trigger is UNCalendarNotificationTrigger }
                    .sorted(by: { (r1, r2) -> Bool in
                        let r1Trigger = r1.trigger as! UNCalendarNotificationTrigger
                        let r2Trigger = r2.trigger as! UNCalendarNotificationTrigger
                        let r1Date = r1Trigger.nextTriggerDate()!
                        let r2Date = r2Trigger.nextTriggerDate()!

                        return r1Date.compare(r2Date) == .orderedAscending
                    })

                let identifiers = notificationRequests.map { $0.identifier }
                UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)

                notificationRequests.enumerated().forEach { (index, request) in
                    if let trigger = request.trigger {
                        let content = UNMutableNotificationContent()
                        content.body = request.content.body
                        content.sound = .default()
                        content.badge = (index + 1) as NSNumber

                        let request = UNNotificationRequest(identifier: request.identifier, content: content, trigger: trigger)
                        UNUserNotificationCenter.current().add(request)
                    }
                }
            }
        }
    } else if let pendingNotifications = UIApplication.shared.scheduledLocalNotifications, pendingNotifications.count > 0 {
        let notifications = pendingNotifications
            .filter { $0.fireDate != nil }
            .sorted(by: { n1, n2 in n1.fireDate!.compare(n2.fireDate!) == .orderedAscending })

        notifications.forEach { UIApplication.shared.cancelLocalNotification($0) }
        notifications.enumerated().forEach { (index, notification) in
            notification.applicationIconBadgeNumber = index + 1
            UIApplication.shared.scheduleLocalNotification(notification)
        }
    }
}


It's a tricky problem. Since iOS does not track the badge number of the local notifications for you, it is up to you to maintain that number of each notification and update them timely.

And just to make it more complicated, the function nextTriggerDate which provided in the UNTimeIntervalNotificationTrigger class doesn't work properly. So if you rely on that to order the pending notifications with the sending one, there will be chaos.

One practical and simplified solution I've managed to found is to first remove all notification then re-send them based on your logics whenever you need to send a new notification. That way the badge number of them are guaranteed to be all correct.

In conclusion, you should do such actions at at least these entries:

  1. userNotificationCenter(_:didReceive:withCompletionHandler:)
  2. userNotificationCenter(_:willPresent:withCompletionHandler:)
  3. where you send out a new notification.
  4. Where you set your app's badgenumber to zero.
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜