Launch an Alert from a Closure Safely

Your app can get into problems when you launch UI from others threads, such as closures. 

For example, you  might have an app that going to ask for permissions for things like photos, notifications, or location data. The system usually handles those, but you might want to be even more exclusive on what to use, and use your own alert. In this tip, I’ll show you how to launch an alert from a closure. 

I’ll use an slightly modified exercise file from the iOS and watchOS App Development:Notifications course I have in the LinkedIn Learning library. If you haven’t seen the course yet, notifications need permissions, and in that exercise file I show how to set up permissions. Since the demo app requires notifications, if the user prohibits notifications, I want the app to tell them that’s a problem. 

I’ll use this method I wrote in an earlier tip to launch the settings

let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in
            if let appSettings = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(appSettings, options: [:], completionHandler: nil)
            }
        }

I’ll use this alert in my user permissions. I have two actions in my app that send notifications. Before they do, they check if they are allowed to using the getNotificationSettings method. That method  runs  a closure. In that closure, add the  code to run the alert if denied or not determined on schedulePizza and makePizza UIActions.

if status == .denied || status == .notDetermined{
    self.accessDeniedAlert()
    return
}

I’ll erase content and settings on my iPhone XR simulator to have a clean slate for permissions. Once it is ready, I’ll run the app. 

I’ll get the permissions alert from the system. I’ll hit Don’t allow, and try to schedule a pizza. We get the alert.

However, look at the console in Xcode.

2019-06-19 06:42:58.245095-0500 Huli Pizza Notification[14948:369926] [Assert] Cannot be called with asCopy = NO on non-main thread.

You have an error message, one I would not ignore. 

The app is running a UI thread of an alert on a non UI thread. The closure is creates a new thread, but alerts really need to be on the main thread where all the User interface is. There’s a huge problem with this that might not be easy to track if you are not aware of it. Because this alert is not running on the main thread, the main thread barrels on, running code without the correct settings. You can get very confused tracking down what happened with that. 

You must run UI on the main thread so the rest of the system responds correctly. Fortunately that is easy. Just state what code you want running on the main thread.   You can assign something to the main thread with he main singleton of the DispatchQueue class. I’ll change the code for a denied permission in schedulePizza and makePizza to this:

if status == .denied || status == .notDetermined{
    DispatchQueue.main.async {
        self.accessDeniedAlert()
    }
    return
}

Clean and Run again, and now we don’t get the error message.  I’m using an example from a notification, but anywhere you plan to call an object on the main thread, such as presenting an alert or changing a label, but doing so from a closure, make sure you assign the object to the main thread before doing so.  

The Not So Whole Code

This week’s project has a big one backing it, so I’m only going to show you the view controller here. Download the project from Github for the full project.

//
//  ViewController.swift
//  Huli Pizza Notification
//
//  Created by Steven Lipton on 11/23/18.
//  Copyright © 2018 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications
// a global constant
let pizzaSteps = ["Make pizza", "Roll Dough", "Add Sauce", "Add Cheese", "Add Ingredients", "Bake", "Done"]


class ViewController: UIViewController {
    var counter = 0
   
    @IBAction func schedulePizza(_ sender: UIButton) {
        UNUserNotificationCenter.current().getNotificationSettings { (settings) in
            let status = settings.authorizationStatus
            if status == .denied || status == .notDetermined{
                DispatchQueue.main.async {
                    self.accessDeniedAlert()
                }
                return
            }
            self.introNotification()
        }
    }
    
    
    @IBAction func makePizza(_ sender: UIButton) {
        UNUserNotificationCenter.current().getNotificationSettings { (settings) in
            let status = settings.authorizationStatus
            if status == .denied || status == .notDetermined{
                DispatchQueue.main.async {
                   self.accessDeniedAlert()
                }
                return
            }
            self.introNotification()
        }
        
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        navigationController?.isNavigationBarHidden = true
    }

    //MARK: - Support Methods
    
    // A function to print errors to the console
    func printError(_ error:Error?,location:String){
        if let error = error{
            print("Error: \(error.localizedDescription) in \(location)")
        }
    }
    
    //A sample local notification for testing
    func introNotification(){
        // a Quick local notification.
        let time = 15.0
        counter += 1
        //Content
        let notifcationContent = UNMutableNotificationContent()
        notifcationContent.title = "Hello, Pizza!!"
        notifcationContent.body = "Just a message to test permissions \(counter)"
        notifcationContent.badge = counter as NSNumber
        //Trigger
        let trigger = UNTimeIntervalNotificationTrigger(timeInterval: time, repeats: false)
        
        //Request
        let request = UNNotificationRequest(identifier: "intro", content: notifcationContent, trigger: trigger)
        //Schedule
        UNUserNotificationCenter.current().add(request) { (error) in
            self.printError(error, location: "Add introNotification")
        }
    }
    //An alert to indicate that the user has not granted permission for notification delivery.
    func accessDeniedAlert(){
        // presents an alert when access is denied for notifications on startup. give the user two choices to dismiss the alert and to go to settings to change thier permissions.
        let alert = UIAlertController(title: "Huli Pizza", message: "Huli Pizza needs notifications to work properly, but they are currently turned off. Turn them on in settings.", preferredStyle: .alert)
        let okayAction = UIAlertAction(title: "Dismiss", style: .default, handler: nil)
        let settingsAction = UIAlertAction(title: "Settings", style: .default) { (action) in
            if let appSettings = URL(string: UIApplication.openSettingsURLString) {
                UIApplication.shared.open(appSettings, options: [:], completionHandler: nil)
            }
        }
        alert.addAction(okayAction)
        alert.addAction(settingsAction)
        present(alert, animated: true) {
        }
    }
}

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: