IOS 10.2 UserNotifications problems on simple Alert and Badge
Asked Answered
P

3

8

Before writing this post I made a lot of researches on UserNotification Framework, that substituted UILocalNotification in IOS 10. I also followed this tutorial to learn everything about this new feature : http://useyourloaf.com/blog/local-notifications-with-ios-10/.

Today I'm encountering so much troubles to implement such trivial Notifications and since it's a recent new feature I couldn't find any solutions (especially in objective C)! I currently have 2 different notifications, one Alert and one Badge updtate.

The Alert Issue

Before Updating my Phone from IOS 10.1 to 10.2, I made an alert on the Appdelegate that is triggered immediatly whenever the user closes the app manually:

-(void)applicationWillTerminate:(UIApplication *)application {
        NSLog(@"applicationWillTerminate");

        // Notification terminate
        [self registerTerminateNotification];
}

// Notification Background terminate 
-(void) registerTerminateNotification {
    // the center
    UNUserNotificationCenter * notifCenter = [UNUserNotificationCenter currentNotificationCenter];

    // Content
    UNMutableNotificationContent *content = [UNMutableNotificationContent new];
    content.title = @"Stop";
    content.body = @"Application closed";
    content.sound = [UNNotificationSound defaultSound];
    // Trigger 
    UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
    // Identifier
    NSString *identifier = @"LocalNotificationTerminate";
    // création de la requête
    UNNotificationRequest *terminateRequest = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
    // Ajout de la requête au center
    [notifCenter addNotificationRequest:terminateRequest withCompletionHandler:^(NSError * _Nullable error) {
        if (error != nil) {
            NSLog(@"Error %@: %@",identifier,error);
        }
    }];
}

Before IOS 10.2 it worked just fine, when I closed the app manually, an alert showed up. But since I updated to IOS 10.2, nothing shows up without any reason, I havent change anything, and I can't see what's missing..

The Badge Issue

I also tried (only in IOS 10.2 this time) to implement badging on my app icon which worked just fine, until I tried to remove it. Here is the function that does it :

+(void) incrementBadgeIcon {
    // only increment if application is in background 
    if ([[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground){

        NSLog(@"increment badge");

        // notif center 
        UNUserNotificationCenter *notifCenter = [UNUserNotificationCenter currentNotificationCenter];

        // Content
        UNMutableNotificationContent *content = [UNMutableNotificationContent new];
        content.badge = [NSNumber numberWithInt:1];
        // Trigger 
        UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1 repeats:NO];
        // Identifier
        NSString *identifier = @"LocalNotificationIncrementBadge";
        // request
        UNNotificationRequest *incrementBadgeRequest = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
        // Ajout de la requête au center
        [notifCenter addNotificationRequest:incrementBadgeRequest withCompletionHandler:^(NSError * _Nullable error) {
            if (error != nil) {
                NSLog(@"Error %@: %@",identifier,error);
            }
        }];
    }
}

For now it does not increment the badge number, as it's name should suggest, but it just set the badge number to 1. Documentation says that if you set content.badge to 0, it removes it, but this does not work. I tried with other numbers, when I manually change it to '2', '3', etc... it changes, but if I set it to 0, it does not work.

Also, in the tutorial I linked earlier, it is mentionned several functions as getPendingNotificationRequests:completionHandler: and getDeliveredNotificationRequests:completionHandler:. I noticed that, when I call these functions right after calling incrementBadgeIcon, if the content.badge is set to '1', '2' etc... it appears in the pending notifications list. However, when I set it to 0, it does not appear anywhere. I get no error, no warning in Xcode, and my application badge still remain.

Does anyone know how I can fix these two Alerts?

Thank in advance

PS: I also tried to use removeAllPendingNotificationRequests and removeAllDeliveredNotifications for both without success.

Plato answered 18/12, 2016 at 16:22 Comment(0)
P
4

Well then I finally managed to make these two alerts functionning. As if posting this question on stackoverflow helped me to open my mind on that subject that hold me for the last few days (also these are really simple answers which is pretty shameful).

Here are my solutions if somebody come accross this post.

The Alert issue

For the alert that should show up when the app is closes, for instance when the app is killed by the user when in background, the code snippet is 'correct' overall. The point is that, when the appDelegate trigger the applicationWillTerminate: function, the system has already started to dealloc/dismantle the whole memory of your application. Therefore, if your app has many views loaded, and many datas to free, the thread that add the notification to the center has enough time to do it's task. But if the application has only few memory to dispose of, the notification is never added to the notifications center's queue.

- (void)applicationWillTerminate:(UIApplication *)application {
    NSLog(@"applicationWillTerminate");

    // Notification terminate
    [Utils closePollenNotification];

    // Pause the termination thread
    [NSThread sleepForTimeInterval:0.1f];

}

So in my case I added a simple sleep in applicationWillTerminate right after creating the notification, which give enough time for it to be registered. (Note: I don't know if this is a good practice but it worked for me).

The Badge Issue

Obviously, after a better understanding of the Apple documentation, setting content.badge to 0 does not remove the previous badge set. It just tells the notification not to update the badge. To remove it, I simply had to call sharedApplication function :

//Reset badge icon
+(void) resetBadgeIcon {
    NSLog(@"reset badge");

    // remove basge
    [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
}

So simple.

Hope this can help somebody.

Plato answered 19/12, 2016 at 3:24 Comment(4)
Have you submitted a bug report regarding the setting the badge value to 0 via local notification? I'm running into the same problem. According to the docs: "When the number in this property is 0, the system does not display a badge. When the number is greater than 0, the system displays the badge with the specified number." The class of badge is an NSNumber?, so there's a difference between nil and 0 that should be respected.Virescence
I think the documentation is correct, thought a bit confusing. When you create a UNMutableNotification you can set a panel of properties, including badge number. If you don't specify the badge number, then it is set to "0". It does not send the message "clear the app icon" it just says "no modifications on badge icon". So I don't think it's a bugPlato
setting applicationIconBadgeNumber to zero has the unintended consequence of clearing all alerts in the notification center. If this is undesirable in your app, see my solution.Gluteal
Aso UIApplication.sharedApplication.applicationIconBadgeNumber is only available in the app itself. Can't be used e.g. from an app extension.Pence
V
5

Regarding the alert:

It's possible that your app is still in the foreground when your local notification fires, so you'll need to implement a delegate method in order for the notification to do anything. For instance, defining this method in your delegate will allow the notification to display an alert, make a sound, and update the badge:

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.alert,.badge,.sound])
}

Regarding the badge:

I have observed that creating a UNMutableNotificationContent object and specifying only the badge value (as an NSNumber object) works for all badge values except 0 (i.e., you cannot clear the badge in this way). I haven't found any documentation for why 0 would behave differently than any other value, especially since the .badge property is defined as an NSNumber?, so the framework should be able to differentiate between nil (no change) and 0 (clear the badge).

I have filed a radar against this.

As a work around, I've found that setting the title property on the UNMutableNotificationContent object, with a badge value of NSNumber(value: 0) does fire. If the title property is missing, it will not fire.

Adding the title property still does not present an alert to the user (Update: this is no longer the case in iOS 11!), so this is a way to silently update the badge value to 0 without needing to invoke the UIApplication object (via UIApplication.shared.applicationIconBadgeNumber = 0).

Here's the entirety of the code in my example project; there's a MARK in the ViewController code showing where inserting the title property resolves the problem:

//
//  AppDelegate.swift
//  userNotificationZeroBadgeTest
//
//  Created by Jeff Vautin on 1/3/17.
//  Copyright © 2017 Jeff Vautin. All rights reserved.
//

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound]) { (success, error) -> Void in
            print("Badge auth: \(success)")
        }

        // For handling Foreground notifications, this needs to be assigned before finishing this method
        let vc = window?.rootViewController as! ViewController
        let center = UNUserNotificationCenter.current()
        center.delegate = vc

        return true
    }
}

//
//  ViewController.swift
//  userNotificationZeroBadgeTest
//
//  Created by Jeff Vautin on 1/3/17.
//  Copyright © 2017 Jeff Vautin. All rights reserved.
//

import UIKit
import UserNotifications

class ViewController: UIViewController, UNUserNotificationCenterDelegate {

    @IBAction func start(_ sender: Any) {
        // Reset badge directly (this always works)
        UIApplication.shared.applicationIconBadgeNumber = 0


        let center = UNUserNotificationCenter.current()

        // Schedule badge value of 1 in 5 seconds
        let notificationBadgeOneContent = UNMutableNotificationContent()
        notificationBadgeOneContent.badge = NSNumber(value: 1)
        let notificationBadgeOneTrigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 1*5, repeats: false)
        let notificationBadgeOneRequest = UNNotificationRequest.init(identifier: "1", content: notificationBadgeOneContent, trigger: notificationBadgeOneTrigger)
        center.add(notificationBadgeOneRequest)

        // Schedule badge value of 2 in 10 seconds
        let notificationBadgeTwoContent = UNMutableNotificationContent()
        notificationBadgeTwoContent.badge = NSNumber(value: 2)
        let notificationBadgeTwoTrigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 2*5, repeats: false)
        let notificationBadgeTwoRequest = UNNotificationRequest.init(identifier: "2", content: notificationBadgeTwoContent, trigger: notificationBadgeTwoTrigger)
        center.add(notificationBadgeTwoRequest)

        // Schedule badge value of 3 in 15 seconds
        let notificationBadgeThreeContent = UNMutableNotificationContent()
        notificationBadgeThreeContent.badge = NSNumber(value: 3)
        let notificationBadgeThreeTrigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 3*5, repeats: false)
        let notificationBadgeThreeRequest = UNNotificationRequest.init(identifier: "3", content: notificationBadgeThreeContent, trigger: notificationBadgeThreeTrigger)
        center.add(notificationBadgeThreeRequest)

        // Schedule badge value of 4 in 20 seconds
        let notificationBadgeFourContent = UNMutableNotificationContent()
        notificationBadgeFourContent.badge = NSNumber(value: 4)
        let notificationBadgeFourTrigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 4*5, repeats: false)
        let notificationBadgeFourRequest = UNNotificationRequest.init(identifier: "4", content: notificationBadgeFourContent, trigger: notificationBadgeFourTrigger)
        center.add(notificationBadgeFourRequest)

        // Schedule badge value of 0 in 25 seconds
        let notificationBadgeZeroContent = UNMutableNotificationContent()
        // MARK: Uncommenting this line setting title property will cause notification to fire properly.
        //notificationBadgeZeroContent.title = "Zero!"
        notificationBadgeZeroContent.badge = NSNumber(value: 0)
        let notificationBadgeZeroTrigger = UNTimeIntervalNotificationTrigger.init(timeInterval: 5*5, repeats: false)
        let notificationBadgeZeroRequest = UNNotificationRequest.init(identifier: "0", content: notificationBadgeZeroContent, trigger: notificationBadgeZeroTrigger)
        center.add(notificationBadgeZeroRequest)
    }

    @IBAction func listNotifications(_ sender: Any) {
        let center = UNUserNotificationCenter.current()
        center.getDeliveredNotifications() { (notificationArray) -> Void in
            print("Delivered notifications: \(notificationArray)")
        }
        center.getPendingNotificationRequests() { (notificationArray) -> Void in
            print("Pending notifications: \(notificationArray)")
        }
    }

    // MARK: UNUserNotificationCenterDelegate

    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        print("Received notification: \(notification)")
        completionHandler([.alert,.badge,.sound])
    }
}
Virescence answered 2/1, 2017 at 19:27 Comment(2)
Workaround with setting the sound property (no title needed) causes the notification to properly trigger. Works also for iOS11. Create yourself a blank sound file and do content.sound = UNNotificationSound.init(named: "blank.aiff").Limbourg
… but, playing a sound also triggers a vibration, so this is still not a 'silent' solution.Virescence
P
4

Well then I finally managed to make these two alerts functionning. As if posting this question on stackoverflow helped me to open my mind on that subject that hold me for the last few days (also these are really simple answers which is pretty shameful).

Here are my solutions if somebody come accross this post.

The Alert issue

For the alert that should show up when the app is closes, for instance when the app is killed by the user when in background, the code snippet is 'correct' overall. The point is that, when the appDelegate trigger the applicationWillTerminate: function, the system has already started to dealloc/dismantle the whole memory of your application. Therefore, if your app has many views loaded, and many datas to free, the thread that add the notification to the center has enough time to do it's task. But if the application has only few memory to dispose of, the notification is never added to the notifications center's queue.

- (void)applicationWillTerminate:(UIApplication *)application {
    NSLog(@"applicationWillTerminate");

    // Notification terminate
    [Utils closePollenNotification];

    // Pause the termination thread
    [NSThread sleepForTimeInterval:0.1f];

}

So in my case I added a simple sleep in applicationWillTerminate right after creating the notification, which give enough time for it to be registered. (Note: I don't know if this is a good practice but it worked for me).

The Badge Issue

Obviously, after a better understanding of the Apple documentation, setting content.badge to 0 does not remove the previous badge set. It just tells the notification not to update the badge. To remove it, I simply had to call sharedApplication function :

//Reset badge icon
+(void) resetBadgeIcon {
    NSLog(@"reset badge");

    // remove basge
    [UIApplication sharedApplication].applicationIconBadgeNumber = 0;
}

So simple.

Hope this can help somebody.

Plato answered 19/12, 2016 at 3:24 Comment(4)
Have you submitted a bug report regarding the setting the badge value to 0 via local notification? I'm running into the same problem. According to the docs: "When the number in this property is 0, the system does not display a badge. When the number is greater than 0, the system displays the badge with the specified number." The class of badge is an NSNumber?, so there's a difference between nil and 0 that should be respected.Virescence
I think the documentation is correct, thought a bit confusing. When you create a UNMutableNotification you can set a panel of properties, including badge number. If you don't specify the badge number, then it is set to "0". It does not send the message "clear the app icon" it just says "no modifications on badge icon". So I don't think it's a bugPlato
setting applicationIconBadgeNumber to zero has the unintended consequence of clearing all alerts in the notification center. If this is undesirable in your app, see my solution.Gluteal
Aso UIApplication.sharedApplication.applicationIconBadgeNumber is only available in the app itself. Can't be used e.g. from an app extension.Pence
G
0

This fix only works on older versions of iOS. Use only for backward compatibility.

This is a fix for the badge issue OP describes (but not the alert issue).

Step 1: Send the notification with badge = negative 1

On your UNMutableNotificationContent set content.badge = @-1 instead of 0. This has 2 benefits:

  • unlike 0, -1 actually clears the badge (if you do step 2 as well)
  • unlike [UIApplication sharedApplication].applicationIconBadgeNumber = 0 it won't clear alerts from your app in notification center.

Step 2: Implement the UNUserNotificationCenterDelegate

You'll need to implement the UNUserNotificationCenterDelegate and do the following:

  • Implement the willPresentNotification method in your delegate, and call completionHandler(UNNotificationPresentationOptionBadge) in the body. Note: I check the request id, and call the default UNNotificationPresentationOptionNone unless this is specifically a notifications designed to clear the badge.
  • Don't forget to retain your delegate instance somewhere with a strong pointer. The notification center does not keep a strong pointer.

After these 2 changes, you can again clear the badge with a local notification.

Gluteal answered 8/2, 2017 at 1:2 Comment(1)
A notification with only content.badge = @-1 doesn't fire when I try to replicate this.Virescence

© 2022 - 2024 — McMap. All rights reserved.