NSAlert.runModal() Crashed when called from Task.init {}

337 views Asked by At

Can anyone tell me why NSAlert.runModal() is crashing when called from inside the Task.init {} function below.

Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1a4122a18)

The logs report that this is being run on the main thread so why would this crash ?

If I wrap it in DispatchQueue.main.async { } then it works fine.

The isBusy property is bound to the enabled state of the start button and this does not complain about being called from a background thread.

Is this a bug or am I doing something fundamentally wrong with my use of Task.init {} to wrap async functions in a viewController ?

class OSViewController: NSViewController {
    
    @IBOutlet weak var ringBar: OSPlainCircularProgressBar!
    
    @IBOutlet weak var upBar: OSProgressBar!
    @IBOutlet weak var downBar: OSProgressBar!
    
    @objc dynamic var progress: Double = 0.0 {
        didSet {
            ringBar.progress = progress / 100.0
            upBar.progress = progress / 100.0
            downBar.progress = progress / 100.0
        }
    }
    
    @objc dynamic var isBusy = false
    
    let asyncObj = AsyncObject(name: "Some Object")
    
    var progressCallback: (Double)->Void {
        return {progress in
            DispatchQueue.main.async { self.progress = progress }
        }
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Do any additional setup after loading the view.
    }
    
    override var representedObject: Any? {
        didSet {
            // Update the view, if already loaded.
        }
    }
    
    @IBAction func start(_ sender: Any) {
        
        isBusy = true
        log("start")
        
        
        Task.init {
            
            log("sync start")
            let (result, message) = await asyncObj.process(5, progress: progressCallback)
            isBusy = false
            log(message)
            log("sync end")  // Reports as running in main thread
            
            
            // CRASHES HERE !
            let alert = self.dialogOK(question: "Some question", text: "Some thing else")
            
            
            
            
            log("task end") // Reports as running in main thread
        }
        
    }
    
    func dialogOK(question: String, text: String) -> Bool {
        let alert = NSAlert()
        alert.messageText = question
        alert.informativeText = text
        alert.alertStyle = .warning
        alert.addButton(withTitle: "OK")
        return alert.runModal() == .alertFirstButtonReturn
    }

    /// This process MUST run on a background thread - it cannot run on the main thread
    /// So how do I force it to run on a background thread ?
    func process(_ count: Int, progress: @escaping (Double)->Void) async -> (Bool, String)  {
        
        // Still on the main thread here ??
        log("1 First part on the main thread ??")
        
        let task = Task.detached { () -> (Bool, String) in
            
            //Thread.sleep(forTimeInterval: 0.1)
            log("2 Run a loop in the background")
            log("  Thread: \(Thread.isMainThread ? "UI" : "BG")")
            
            var cnt = 0
            for i in 0..<count {
                Thread.sleep(forTimeInterval: 0.025)// sleep(seconds: 0.1)
                log("  i: \(i)")
                cnt += 1
                progress (Double(cnt)/Double(count)*100)
                
            }
            log("  >>>>")
            
            return (true, "Background task Done")
            
        }
        
        let result = await task.value
        
        log("2 First part on the main thread ??")
        return result
        
    }
    

    
}

func log(_ msg: String) {
    
    let type = Thread.isMainThread ? "UI" : "BG"
    
    print("\(type): \(msg)")
}

extension OSViewController {
    func sleep(seconds: Double) async {
        await Task.sleep(UInt64(seconds * 1000000000.0))
    }
}

class AsyncObject {
    var name: String = "Someobject"
    
    init(name: String){
        self.name = name
    }
    
    /// This process MUST run on a background thread - it cannot run on the main thread
    /// So how do I force it to run on a background thread ?
    @MainActor func process(_ count: Int, progress: @escaping (Double)->Void) async -> (Bool, String)  {
        
        // Still on the main thread here ??
        log("1 First part on the main thread ??")
        log("  Object is \(self.name)")
        
        let task = Task.detached { () -> (Bool, String) in
            
            //Thread.sleep(forTimeInterval: 0.1)
            log("2 Run a loop in the background")
            log("  Thread: \(Thread.isMainThread ? "UI" : "BG")")
            
            var cnt = 0
            for i in 0..<count {
                Thread.sleep(forTimeInterval: 0.025)// sleep(seconds: 0.1)
                log("  i: \(i)")
                cnt += 1
                progress (Double(cnt)/Double(count)*100)
                
            }
            log("  >>>>")
            
            return (true, "Background task Done")
            
        }
        
        let result = await task.value
        
        log("2 First part on the main thread ??")
        return result
        
    }
}

EDIT: This seems to work

@IBAction func start(_ sender: Any) {
    
    isBusy = true
    log("start")
    
    
    Task.init {
        
        log("sync start")
        let (result, message) = await asyncObj.process(5, progress: progressCallback)
        isBusy = false
        log(message)
        log("sync end")  // Reports as running in main thread
        
        
        // NO CRASHES HERE !
        self.alert = self.dialogOK(question: "Some question", text: "Some thing else")
        
        if let win = self.view.window {
            self.alert?.beginSheetModal(for: win) {result in
                self.alert = nil
            }
        }
        
        
        
        log("task end") // Reports as running in main thread
    }
    
}
0

There are 0 answers