Cocoa: how to run a modal window while performing a background task?
I've tried calling
modalSession=[NSApp beginModalSessionForWindow:conversionWindow];
[NSApp runModalForWindow:conversionWindow];
in order to get a modal conversionWindow that prevents the user to interact with the rest of the application, but this also seems to block code execution. What I mean is tha开发者_StackOverflow中文版t the code that comes after the code shown above isn't executed at all. How can I fix this? I'm sure this is possible because many applications show some progress while performing some big task, like video conversion etc...
Please don't use an app-modal window unless it's absolutely necessary. Use a sheet if possible. However, if you must use a modal dialog, you can make the main run loop run by giving it some time while the modal dialog is open:
NSModalSession session = [NSApp beginModalSessionForWindow:[self window]];
int result = NSRunContinuesResponse;
while (result == NSRunContinuesResponse)
{
//run the modal session
//once the modal window finishes, it will return a different result and break out of the loop
result = [NSApp runModalSession:session];
//this gives the main run loop some time so your other code processes
[[NSRunLoop currentRunLoop] limitDateForMode:NSDefaultRunLoopMode];
//do some other non-intensive task if necessary
}
[NSApp endModalSession:session];
This is very useful if you have views that require the main run loop to operate (WebView
comes to mind).
However, understand that a modal session is just that, and any code after the call to beginModalSessionForWindow:
will not be executed until the modal window closes and the modal session ends. This is one very good reason not to use modal dialogs.
Note that you must not do any significant work in the while
loop in the code above, because then you will block your modal session as well as the main run loop, which will turn your app into beachball city.
If you want to do something substantial in the background you must use some form of concurrency, such as a using NSOperation
, a GCD background queue or just a plain background thread.
You need to start the background "task" in another thread if you want it to run while a modal window is up. But in most cases, the best solution is to not use a modal window.
That is how a modal window works: the window is presented synchronously, and the code after NSApp.runModal(for: window)
is executed after the modal window session is stopped.
Under the hood, the modal window runs in a special modal event loop, so that only the events happen in the specified window are processed.
In code, it does stop the execution at the point of NSApp.runModal(for: window)
, but you can still dispatch jobs from the window. For example, a button on the modal window that triggers a download task.
You can use:
- performSelector(onMainThread:with:waitUntilDone:modes:) to dispatch a job.
- GCD to dispatch a job.
One common scenario that you feel the modal window blocks code execution can be:
// present a modal window in GCD main queue asynchronously:
DispatchQueue.main.async {
NSApp.runModal(for: window)
// only executes after modal window is dismissed
}
and somewhere else tries to dispatch using GCD. For example:
// modal window's button action handler:
button.actionHandler = {
DispatchQueue.main.async {
// some button action...
}
}
Because NSApp.runModal(for: window)
doesn't leave the dispatched block, the GCD main queue won't execute following blocks until the modal window is dismissed.
To avoid this blocking issue, you could:
- Avoid calling
NSApp.runModal(for: window)
inDispatchQueue.main
. - Use performSelector(onMainThread:with:waitUntilDone:modes:) to dispatch a job.
For the 2nd solution, I made a convenient helper for myself:
public func onMainThread(waitUntilDone: Bool = false, block: @escaping BlockVoid) {
_ = MainRunloopDispatcher(waitUntilDone: waitUntilDone, block: block)
}
private final class MainRunloopDispatcher: NSObject {
private let block: BlockVoid
init(waitUntilDone: Bool = false, block: @escaping BlockVoid) {
self.block = block
super.init()
performSelector(onMainThread: #selector(execute), with: nil, waitUntilDone: waitUntilDone)
}
@objc private func execute() {
block()
}
}
So that when the GCD main queue is blocked, you can use to call some code on the main thread
onMainThread {
// here is on main thread, update UI...
}
To read more, this article helped me.
精彩评论