In my Cocoa application I have some computations done in the background. The background work is runned with DispatchQueue.global(qos: .utility).async
.
This background task may report errors by showing a modal NSAlert
via DispatchQueue.main.async
.
Also, in my application user can run NSOpenPanel
to open some files (with NSOpenPanel.runModal
).
The problem is that if the user opens NSOpenPanel
and at the same time the background task shows NSAlert
, the application may hang.
- user opens modal
NSOpenPanel
- background task opens modal
NSAlert
atop theNSOpenPanel
- user clicks Close inside
NSOpenPanel
(it really can accessNSOpenPanel
despite the more modalNSAlert
present). - both
NSAlert
andNSOpenPanel
being closed and application hangs with main thread blocked insideNSOpenPanel.runModal()
- application will not hang if user first close
NSAlert
and thenNSOpenPanel
.
The minimal code sample (the test IBaction is binded as the action for the button in the main window)
import Cocoa
@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
@IBOutlet weak var window: NSWindow!
@IBAction func test(_ sender: Any) {
//run some work in background
DispatchQueue.global(qos: .utility).async
{
sleep(1) //some work
//report errors in the main thread.
DispatchQueue.main.async {
let alert = NSAlert();
alert.informativeText = "Close NSOpen panel before this alert to reproduct the hang."
alert.runModal()
}
}
//user want to open a file and opens the open file dialog
let dlg = NSOpenPanel();
dlg.runModal();
}
}
So, what's wrong with this code and why it cause the hang in particular use cases? And how can I prevent such a hang?
Additional note: I discovered, then if I replace dlg.runModal()
with NSApp.RunModal(for: dlg)
(which is exactly the same as per Apple documentation), this will fix hang in the usecase described above.
But it still will close NSAlert
automatically right after closing NSOpenPanel
. And I still can't understood why this behave as it does.
Update
I updated the code above to include full code of AppDelegate class of minimal reproducible application. To reproduce the case, just create new SwiftApp in XCode, replace AppDelegate code, add button in the main window and wire button's action with test
func. I also placed full ready-to-compile project on github: https://github.com/snechaev/hangTest
The code for configuring NSOpenPanel and NSAlert along with their results handling is excluded as such a code does not affect the hang.