Usernotification framework badge does not increase
Asked Answered
K

2

8

I am using UserNotification framework in my app and sending local notifications (not push notifications), and I want to set the badge to the number of notifications received so what I did was to set the number of notifications received into a user default then I tried to assign the value to the badge to get me a badge number but the badge number would not increase. This is my code below

To set value of received notification

center.getDeliveredNotifications { notification in

    UserDefaults.standard.set(notification.count, forKey: Constants.NOTIFICATION_COUNT)
    print("notification.count \(notification.count)")
    print(".count noti \(UserDefaults.standard.integer(forKey: Constants.NOTIFICATION_COUNT))")

}

This accurately prints the number of notification received and when I decided to set it to my badge it only shows 1

content.badge = NSNumber(value: UserDefaults.standard.integer(forKey: Constants.NOTIFICATION_COUNT))

I have no idea why the value does not increase every time. Any help would be appreciated.

Or if it is possible to always update the badge anywhere in the app.

Killie answered 14/9, 2018 at 1:7 Comment(11)
I don't see a single line suggesting that a value has increased or changed.Streeto
What am I missing?Killie
You're missing any explanation of why you think the value would increase. You are not increasing it, so what do you expect would increase it?Rectangle
How would I get the badge to show the number of notifications then. I couldn’t get anything on the documentationsKillie
Which badge are you trying to set? The application icon badge you see on iOS main screen?Brod
Where are you calling center.getDeliveredNotifications?Dispersal
I dont know where to call it. it is called in the Viewcontroller where the object that sets the notification is triggeredKillie
@Killie I removed my downvote on Carpsen90's answer and upvoted it. But it's a highly complex answer as of now.Piddling
I awarded the closest answer but no answer really solved the problemKillie
@Killie Can you comment on the accepted answer and explain why it didn't solve your problem?Piddling
So the was a notification works is to display things that a user is to take note of at that particular point in time. So if I set a reminder for 4PM tomorrow and another for 5PM next week, once it’s 4PM tomorrow I ought to get notified they it’s 4PM and that’s the notification but what the written code does is actually to show the total number of notifications set even if the notification is to be triggered the next year and it sets the app badge as this number. Instead of showing the current notifications delivered.Killie
D
7

Send the local notifications like so:

func sendNotification(title: String, subtitle: String, body: String, timeInterval: TimeInterval) {
    let center = UNUserNotificationCenter.current()
    center.getPendingNotificationRequests(completionHandler: { pendingNotificationRequests in

        //Use the main thread since we want to access UIApplication.shared.applicationIconBadgeNumber
        DispatchQueue.main.sync {

            //Create the new content
            let content = UNMutableNotificationContent()
            content.title = title
            content.subtitle = subtitle
            content.body = body

            //Let's store the firing date of this notification in content.userInfo
            let firingDate = Date().timeIntervalSince1970 + timeInterval
            content.userInfo = ["timeInterval": firingDate]

            //get the count of pending notification that will be fired earlier than this one
            let earlierNotificationsCount: Int = pendingNotificationRequests.filter { request in

                let userInfo = request.content.userInfo
                if let time = userInfo["timeInterval"] as? Double {
                    if time < firingDate {
                        return true
                    } else {

                        //Here we update the notofication that have been created earlier, BUT have a later firing date
                        let newContent: UNMutableNotificationContent = request.content.mutableCopy() as! UNMutableNotificationContent
                        newContent.badge = (Int(truncating: request.content.badge ?? 0) + 1) as NSNumber
                        let newRequest: UNNotificationRequest =
                            UNNotificationRequest(identifier: request.identifier,
                                                  content: newContent,
                                                  trigger: request.trigger)
                        center.add(newRequest, withCompletionHandler: { (error) in
                            // Handle error
                        })
                        return false
                    }
                }
                return false
            }.count

            //Set the badge
            content.badge =  NSNumber(integerLiteral: UIApplication.shared.applicationIconBadgeNumber + earlierNotificationsCount + 1)
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval,
                                                            repeats: false)

            let requestIdentifier = UUID().uuidString  //You probably want to save these request identifiers if you want to remove the corresponding notifications later
            let request = UNNotificationRequest(identifier: requestIdentifier,
                                                content: content, trigger: trigger)

            center.add(request, withCompletionHandler: { (error) in
                // Handle error
            })
        }
    })
}

(You may need to save the requests' identifiers (either in user defaults or core data if you'd like to update them, or even cancel them via removePendingNotificationRequests(withIdentifiers:))

You can call the above function like so:

sendNotification(title: "Meeting Reminder",
                 subtitle: "Staff Meeting in 20 minutes",
                 body: "Don't forget to bring coffee.",
                 timeInterval: 10)

Declare your view controller as a UNUserNotificationCenterDelegate:

class ViewController: UIViewController, UNUserNotificationCenterDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
        UNUserNotificationCenter.current().delegate = self
    }
    //...
}

And to handle interacting with the notification, update the badge of the app, and the badge of the upcoming notifications:

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    //UI updates are done in the main thread
    DispatchQueue.main.async {
        UIApplication.shared.applicationIconBadgeNumber -= 1
    }

    let center = UNUserNotificationCenter.current()
    center.getPendingNotificationRequests(completionHandler: {requests in
        //Update only the notifications that have userInfo["timeInterval"] set
        let newRequests: [UNNotificationRequest] =
            requests
                .filter{ rq in
                    return rq.content.userInfo["timeInterval"] is Double?
                }
                .map { request in
                    let newContent: UNMutableNotificationContent = request.content.mutableCopy() as! UNMutableNotificationContent
                    newContent.badge = (Int(truncating: request.content.badge ?? 0) - 1) as NSNumber
                    let newRequest: UNNotificationRequest =
                        UNNotificationRequest(identifier: request.identifier,
                                              content: newContent,
                                              trigger: request.trigger)
                    return newRequest
        }
        newRequests.forEach { center.add($0, withCompletionHandler: { (error) in
            // Handle error
        })
        }
    })
    completionHandler()
}

This updates the app badge by decreasing it when a notification is interacted with ie tapped. Plus it updates the content badge of the pending notifications. Adding a notification request with the same identifier just updates the pending notification.

To receive notifications in the foreground, and increase the app badge icon if the notification is not interacted with, implement this:

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    DispatchQueue.main.async {
        UIApplication.shared.applicationIconBadgeNumber += 1
    }
    completionHandler([.alert, .sound])
}

Here are some gifs:

  • 1st: Receiving local notifications increases the app badge. Whereas interacting with a notification decreases the app badge.

  • 2nd: Receiving local notifications when the app is killed (I used a trigger timeInterval of 15s in this).

  • 3rd: Receiving notification whilst in the foreground increases the app badge unless the user interacts with it.

The complete class used in my test project looks like this:

import UIKit
import UserNotifications

class ViewController: UIViewController, UNUserNotificationCenterDelegate {
    var bit = true
    @IBAction func send(_ sender: UIButton) {
        let time: TimeInterval = bit ? 8 : 4
        bit.toggle()
        sendNotification(title: "Meeting Reminder",
                         subtitle: "Staff Meeting in 20 minutes",
                         body: "Don't forget to bring coffee.",
                         timeInterval: time)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        UNUserNotificationCenter.current().delegate = self
    }

    func sendNotification(title: String, subtitle: String, body: String, timeInterval: TimeInterval) {
        let center = UNUserNotificationCenter.current()
        center.getPendingNotificationRequests(completionHandler: { pendingNotificationRequests in
            DispatchQueue.main.sync {
                let content = UNMutableNotificationContent()
                content.title = title
                content.subtitle = subtitle
                content.body = body
                let firingDate = Date().timeIntervalSince1970 + timeInterval
                content.userInfo = ["timeInterval": firingDate]
                let earlierNotificationsCount: Int = pendingNotificationRequests.filter { request in
                    let userInfo = request.content.userInfo
                    if let time = userInfo["timeInterval"] as? Double {
                        if time < firingDate {
                            return true
                        } else {
                            let newContent: UNMutableNotificationContent = request.content.mutableCopy() as! UNMutableNotificationContent
                            newContent.badge = (Int(truncating: request.content.badge ?? 0) + 1) as NSNumber
                            let newRequest: UNNotificationRequest =
                                UNNotificationRequest(identifier: request.identifier,
                                                      content: newContent,
                                                      trigger: request.trigger)
                            center.add(newRequest, withCompletionHandler: { (error) in
                                // Handle error
                            })
                            return false
                        }
                    }
                    return false
                    }.count
                content.badge =  NSNumber(integerLiteral: UIApplication.shared.applicationIconBadgeNumber + earlierNotificationsCount + 1)
                let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval,
                                                                repeats: false)

                let requestIdentifier = UUID().uuidString  //You probably want to save these request identifiers if you want to remove the corresponding notifications later
                let request = UNNotificationRequest(identifier: requestIdentifier,
                                                    content: content, trigger: trigger)

                center.add(request, withCompletionHandler: { (error) in
                    // Handle error
                })
            }
        })
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        DispatchQueue.main.async {
            UIApplication.shared.applicationIconBadgeNumber += 1
        }
        completionHandler([.alert, .sound])
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        DispatchQueue.main.async {
            UIApplication.shared.applicationIconBadgeNumber -= 1
        }

        let center = UNUserNotificationCenter.current()
        center.getPendingNotificationRequests(completionHandler: {requests in
            let newRequests: [UNNotificationRequest] =
                requests
                    .filter{ rq in
                        return rq.content.userInfo["timeInterval"] is Double? 
                    }
                    .map { request in
                        let newContent: UNMutableNotificationContent = request.content.mutableCopy() as! UNMutableNotificationContent
                        newContent.badge = (Int(truncating: request.content.badge ?? 0) - 1) as NSNumber
                        let newRequest: UNNotificationRequest =
                            UNNotificationRequest(identifier: request.identifier,
                                              content: newContent,
                                              trigger: request.trigger)
                        return newRequest
            }
            newRequests.forEach { center.add($0, withCompletionHandler: { (error) in
                // Handle error
            })
            }
        })
        completionHandler()
    }
}
Dispersal answered 16/9, 2018 at 6:11 Comment(2)
After the fixes, I've removed my downvote and upvoted this answer. But it's such a complex solution to a simple need. And it's not applicable for notifications which have a 'location' trigger. The answer is only good for notifications with a 'time' trigger. Make sure you see this OTHER chatroom discussion. The main discussion did not happen in the chatroom which Samuel has linked...Piddling
@Honey thank you for helping improve the answer 👍🏻Dispersal
P
1

I'm assuming this all a local notification.

AFAIK there is solution to your question!

When the notification arrives, you're either in foreground or background.

  • foreground: you get the userNotificationCenter(_:willPresent:withCompletionHandler:) callback but I don't think in that case you'll want to increase the badge right? Because the user has just seen it. Though I can imagine where you might need to do such. Suppose your app is like WhatsApp and the user has the app opened and is sending a message to his mother. Then a message from his father arrives. At this point he hasn't opened the messages between him and his father yet he sees the notification. In your willPresent you could query the getDeliveredNotifications and and adjust your badge count.
  • background: for iOS10+ version for local notifications you're out of luck! Because there is NO callback for you. The notification gets delivered to the OS and that's it! There use to be a named application:didReceiveLocalNotification: but that's deprecated. For more on that see here
  • When user taps (foreground or backend) then you'll get the userNotificationCenter(_:didReceive:withCompletionHandler:) but that has no use again because the user has already acknowledged receiving the notification and increasing the badge in this case doesn't make sense.

Long story short AFAIK there is nothing you can do for local notifications.

If it's a remote notification then in the application(_:didReceiveRemoteNotification:fetchCompletionHandler:) you can query the delivered notifications and increase the badge count...

EDIT:

Since the badgeCount is attached to the arriving notification, then if you can update its badgeCount prior to arrival then you're all good. e.g. at 12pm you can always query the list of pendingNotifications. It will give you all the notifications arriving after 12pm and update the badgeCount on them if necessary e.g. decrease their badgeCount if some delivered notifications are read. For a complete solution on this see see Carspen90's answer. The gist of his answer is

for any new notification you want to send:

  1. get the pendingNotifications
  2. filter notifications which their firingDate is sooner than the new to be sent notification and get its count
  3. set the new notification's badge to app's badgeCount + filteredCount + 1
  4. if any of the pending notifications have a firingDate greater than the new notification we just added then we will increase the badgeCount on the pending notification by 1.
  5. obviously again whenever you interact with delivered notifications, then you have to get all pendingNotifications again and decrease their badgeCount by 1

CAVEAT:

You can't do such for notifications which their trigger is based on location because obviously they don't care about time.

Piddling answered 16/9, 2018 at 18:55 Comment(2)
it is local notification. can you give me a code representation for your answer pleaseKillie
Regardless of code did you understand what I said? Ultimately what I'm saying is if you're in the background then the OS doesn't give you a callback for delivering local notifications. Hence you're almost out of luck. Unless you want to increase the badge for the father/mother notification example I gave. But I don't recommend that either because your badge count won't be accurate for when app is in background. And this would confuse the user. At least to my knowledge there is no solution or hack for this.Piddling

© 2022 - 2024 — McMap. All rights reserved.