theory theory - 3 months ago 24
iOS Question

Must `registerUserNotificationSettings:` Be called during app launch cycle?

The docs for

state:


If your app displays alerts, play sounds, or badges its icon, you must call this method during your launch cycle to request permission to alert the user in these ways.


I was disappointed to read this, as it seems rude for the app to ask for permission to send push notifications before there's a need to. In the app I'm designing, for example, the user must create an account with our online service before there's any reason to send push notifications. And it may be that the user never signs up, just uses the app locally, so there's never any reason to ask. But if I can only ask on app launch, it means the user would have to create an account, quit the app, then launch it again before we could ask. Seems odd.

Is this really necessary? I tried putting the call to
registerUserNotificationSettings:
into a more relevant part of the app, but then the app never prompted for permission to send push notifications. Is this just a policy for iOS push notifications, or is there some way to have more flexibility as to when to ask for permission to send push notifications?

Answer

There were two issues here, one mine and one from iOS. My issue was that I was dismissing the view controller that called -registerForNotifications after it returned; I didn't realize that it ran asynchronously. So I added -application:didRegisterUserNotificationSettings to my app delegate. This method is called once the the user has responded to the prompt presented by -registerForNotifications, and I have it post a notification. Before calling -registerForNotifications, I set up a listener for that notification, and only dismiss the view controller once the notification is received. Kind of wish there was a delegate or a block to specify instead, but there's not.

And I suspect that the reason there's not is because iOS really wants you to call -registerForNotifications in application:didFinishLaunchingWithOptions:, so it's natural for the async callback to just be another method in the app delegate. The reason I think this is because my app only receives push remote notifications if this method is application:didFinishLaunchingWithOptions:. I only call it there if it was called elsewhere, but the upshot is that, even though I've approved getting push notifications via -registerForNotifications called outside application:didFinishLaunchingWithOptions:, I have to quit my app -- force quit it -- then start it again so that application:didFinishLaunchingWithOptions: gets called again before the app gets push notifications. Annoying.

So here's the complete pattern:

  • In the app delegate's application:didFinishLaunchingWithOptions: method call -registerForNotifications if a user default has been set:

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        if NSUserDefaults.standardUserDefaults().boolForKey("registedNotificationSettings") {
            self.registerForNotifications()
        }
        return true
    }
    
  • In the app delegate's -application:didRegisterUserNotificationSettings: method, configure for remote notifications, store the user default value, and post a notification:

    func application(application: UIApplication, didRegisterUserNotificationSettings notificationSettings: UIUserNotificationSettings) {
        if notificationSettings.types != .None {
            application.registerForRemoteNotifications()
        }
        let defaults = NSUserDefaults.standardUserDefaults()
        if !defaults.boolForKey( "registedNotificationSettings") {
            defaults.setBool(true, forKey: "registedNotificationSettings")
            defaults.synchronize()
        }
        NSNotificationCenter.defaultCenter().postNotification(
            NSNotification(name: "RegistedNotificationSettings", object: notificationSettings)
        )
    }
    
  • In my view controller, on a button press, listen for that notification, then request permission to send push notifications:

    @IBAction func okayButtonTapped(sender: AnyObject) {
        NSNotificationCenter.defaultCenter().addObserver(
            self,
            selector: #selector(didRegisterNotificationSettings(_:)),
            name: "RegistedNotificationSettings",
            object: nil
        )
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        appDelegate.registerForNotifications()
    }
    
  • Then of course we'll need the notification method, which dismisses the view controller when permission has been granted, and needs to do something else when it hasn't:

    func didRegisterNotificationSettings(notification: NSNotification) {
        let status = notification.object as! UIUserNotificationSettings
        if status.types != .None {
            // Yay!
            self.done()
            return
        }
    
        // Tell them a little more about it.
        NSLog("User declined push notifications")
        self.done()
    }
    

With this configuration, it all works, except that the user will get no push notifications, even if they have granted permission until the app has been restarted and -registerForNotifications called in application:didFinishLaunchingWithOptions:. All this to avoid prompting the user on app startup.

Comments