Callback Method if user declines Push Notification Prompt?

23,348

Solution 1

In iOS 7, when the system's push notification prompt appears, the app becomes inactive and UIApplicationWillResignActiveNotification fires. Similarly when the user responds to the prompt (pressing either Yes or No), the app becomes active again and UIApplicationDidBecomeActiveNotification fires.

So you can listen for this notification, and then hide your loading screen.

Note: While the prompt is displayed, the Home button, Notification Center, and Control Center are disabled so they cannot trigger a false-positive UIApplicationDidBecomeActiveNotification. However if the user presses Lock button it will trigger UIApplicationDidBecomeActiveNotification.

Solution 2

You can always get current allowed notification types from:

UIRemoteNotificationType notificationTypes = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];

Keep in mind user can also disable notification in phone settings.

If you check that on didRegisterForRemoteNotificationsWithDeviceToken you should see if types you asked for are enabled.

Solution 3

Some of the answers here are not relevant anymore, or are more complicated than it should be, since UserNotifications framework and iOS 10 you can easily get this data like so:

let center = UNUserNotificationCenter.current()

// Request permission to display alerts and play sounds.
center.requestAuthorization(options: [.alert, .sound]) 
{ (granted, error) in
  // Enable or disable features based on authorization.
}

Solution 4

Couldn't you just do the following:

- (void)application:(UIApplication *)application didRegisterUserNotificationSettings:(UIUserNotificationSettings *)notificationSettings {
    BOOL pushEnabled = notificationSettings.types & UIUserNotificationTypeAlert;
}

This method should be the callback to that push notifications prompt, and from there, you can check the bitmask to see if push notifications were enabled or not.

Solution 5

Here's how I did it in Swift 3. They key here is to keep track of the application's lifecycle state internally. When the push prompt is presented, the application resigns active, but does not enter the background. This is all in my AppDelegate.swift.

This is a really big hack and is not recommended in production. Apple could change the way these alerts are presented and this could break at any time. This was tested using various iPhones and iPads running iOS 9 and 10.

/// An internal value used to track application lifecycle state
enum ApplicationLifecycleState {
    case willResignActive
    case didEnterBackground
    case willEnterForeground
    case didBecomeActive
    case unknown
}

/// This is used purely for tracking the application lifecycle for handling the system push notification alert
var internalLifecycleState: ApplicationLifecycleState = .unknown {
    didSet {
        // If we're not in the middle of asking for push permissions, none of the below applies, just bail out here
        if !isAskingForPushPermissions { return }

        // WARNING: Application lifecycle trickery ahead
        // The normal application lifecycle calls for backgrounding are as follows:
        // applicationWillResignActive -> applicationDidEnterBackground -> applicationWillEnterForeground -> applicationDidBecomeActive
        // However, when the system push notification alert is presented, the application resigns active, but does not enter the background:
        // applicationWillResignActive -> [user taps on alert] -> applicationDidBecomeActive
        // We can use this discrepancy to our advantage to detect if the user did not allow push permissions

        // If applicationDidBecomeActive
        // AND the previous state was applicationWillResignActive
        // AND the notification types bitmask is 0, we know that the user did not allow push permissions
        // User denied permissions
        if internalLifecycleState == .didBecomeActive
            && oldValue == .willResignActive
            && UIApplication.shared.currentUserNotificationSettings?.types.rawValue == 0 {
            // We're done
            firePushCompletionBlockAndCleanup(registered: false)
        } else {
            // The state below can only be entered on iOS 10 devices.
            // If the user backgrounds the app while the system alert is being shown,
            // when the app is foregrounded the alert will dismiss itself without user interaction.
            // This is the equivalent of the user denying push permissions.
            // On iOS versions below 10, the user cannot background the app while a system alert is being shown.

            if #available(iOS 10, *), internalLifecycleState == .didBecomeActive {
                firePushCompletionBlockAndCleanup(registered: false)
            }
        }
    }
}

/// Used internally to track if the system push notification alert is currently being presented
var isAskingForPushPermissions = false

typealias PushNotificationRegistrationCompletionBlock = ((_ registered: Bool) -> Void)

// ...

func applicationWillResignActive(_ application: UIApplication) {    
    internalLifecycleState = .willResignActive
}

func applicationDidEnterBackground(_ application: UIApplication) {
    internalLifecycleState = .didEnterBackground
}

func applicationWillEnterForeground(_ application: UIApplication) {
    internalLifecycleState = .willEnterForeground
}

func applicationDidBecomeActive(_ application: UIApplication) {
    internalLifecycleState = .didBecomeActive
}

// ...

func setupPushNotifications(_ application: UIApplication = UIApplication.shared, completion: @escaping PushNotificationRegistrationCompletionBlock) {
    isAskingForPushPermissions = true
    pushCompletionBlock = completion
    let settings = UIUserNotificationSettings(types: [.alert, .sound, .badge], categories: nil)
    application.registerUserNotificationSettings(settings)
    application.registerForRemoteNotifications()
}

fileprivate func firePushCompletionBlockAndCleanup(registered: Bool) {
    pushCompletionBlock?(registered)
    pushCompletionBlock = nil
    isAskingForPushPermissions = false
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {

    // application:didRegisterForRemoteNotificationsWithDeviceToken may be called more than once (once for each notification type)
    // By checking that the notification types bitmask is greater than 0, we can find the final time this is called (after the user actually tapped "allow")
    // If the user denied push permissions, this function is never called with a positive notification type bitmask value
    if UIApplication.shared.currentUserNotificationSettings?.types.rawValue ?? 0 > 0 {
        firePushCompletionBlockAndCleanup(registered: true)
    }
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    print("Failed to register for notifications with error: " + error.localizedDescription)
    firePushCompletionBlockAndCleanup(registered: false)
}

Usage:

appDelegate.setupPushNotifications(completion: { [weak self] (registered) in
    // If registered is false, the user denied permissions
})
Share:
23,348
最白目
Author by

最白目

I´m a mobile developer.

Updated on March 01, 2020

Comments

  • 最白目
    最白目 over 4 years

    My problem is I want to show a loading screen for the initial Push Notification Prompt "The app wants to send you push notifications."

    So if the user hits yes I can proceed and start the app in the then invoked delegate methods:

    - (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken
    {
      [self hideLoadingScreen];
    }
    
    - (void)application:(UIApplication*)application didFailToRegisterForRemoteNotificationsWithError:(NSError*)error
    {
      [self hideLoadingScreen];
    }
    

    However if the user hits no, none of these methods get called, which makes sense. My question is, is there a different delegate method that gets fired if he declines?

    My problem is if no is selected, the loading screens never disappear. So I somehow need to know when the user is done with the selection.

  • 最白目
    最白目 over 10 years
    my problem is that didRegisterForRemoteNotificationsWithDeviceToken and didFailToRegisterForRemoteNotificationsWithError never get called if the user hits no on the prompt.
  • 最白目
    最白目 over 10 years
    hmm but when to access the BOOL? there seems to be no callback fired if the user doesnt allow Push Notifications.
  • Puneet Sharma
    Puneet Sharma over 10 years
    You want to disappear the loading screen right? If I am guessing right then you show the user a loading screen at the launch. Then app asks for allowing pushnotification. If allowed then show a different screen, if disallowed then show different screen. Right?
  • 最白目
    最白目 over 10 years
    well, it´s the same screen on allow and disallow, but basically you are right. as soon as the user presses yes or no, the current loading screen should disappear.
  • Puneet Sharma
    Puneet Sharma over 10 years
    One way I can think of is using NSTimer but that wont be user friendly. Another way that may work is intercepting every touch in UIWindow. Here this may help youhttp://stackoverflow.com/questions/2003201/observing-pinc‌​h-multi-touch-gestur‌​es-in-a-uitableview/‌​2003781#2003781
  • 最白目
    最白目 over 10 years
    so you have the same impression as me, that there are no callbacks fired if the user declines push notifications?
  • Puneet Sharma
    Puneet Sharma over 10 years
    Yes that seems true. That Alert View is generated by ios system, independent of the app and I dont think Apple has provided any APIs to access it.
  • 最白目
    最白目 over 10 years
    that´s awful. Luckily we agreed on not showing a loading screen while the pop up appears now.
  • Puneet Sharma
    Puneet Sharma over 10 years
    Good for you. Enjoy your day!!
  • Mark Edington
    Mark Edington over 10 years
    Upvoted, because despite the fact this doesn't provide a direct answer to the original question it does provide a mention of something quite useful that isn't even mentioned in the documentation of registerForRemoteNotificationTypes!
  • Isaac Overacker
    Isaac Overacker over 9 years
    The API for this has changed in the iOS 8 SDK. On iOS >= 8.0, you have to check [[UIApplication sharedApplication] currentUserNotificationSettings].categories.
  • ekscrypto
    ekscrypto over 7 years
    I can confirm that this behaviour is still current in iOS 10
  • gabriel_vincent
    gabriel_vincent about 5 years
    I can confirm that this behaviour is still current in iOS 12
  • Rizwan Ahmed
    Rizwan Ahmed almost 4 years
    I can confirm that this behaviour is still current in iOS 14 beta 2