NSRunAlertPanel caused performance issue on multithreading - objective-c

Occasionally I have to do a popup alert window in my Cocoa code segments. Previously I used NSAlert directly then have runModal to go while I found that the NSRunAlertPanel is more easier to achieve my goal. So I decided to switch all my alert functions to NSRunAlertPanel. It seemed okay at most time。
Now I'm adding multithreading. I found that NSRunAlertPanel appears clearly slower than NSAlert when calling back in the main thread.
Code segments:
Firstly I create a thread:
[NSThread detachNewThreadSelector: #selector(tryRunLoop:) toTarget:self withObject:nil];
Then this functiontryRunLoop in this thread call the alert window function in the main thread:
while(1)
[self performSelectorOnMainThread:#selector(showAlert:) withObject:anObject waitUntilDone:YES];
The function showAlert in main thread do the rest things:
NSRunAlertPanel(#"Warning:",#"Just testing", #"YES", nil, nil);
As time goes by the response of the popup window appears slower and slower.If I use NSAlert instead of NSRunAlertPanel, or did not run the popup method in main thread, the symptom should disappear.
I also found that the CPU usage was also different between these two methods. Obviously NSAlert costs low CPU usage while hitting the button all the time.
Is someone able to explain these phenomenons?
PS: I was not allowed to put the whole original project online so that I've created a simple Cocoa project in Github to simulate the symptom and the URL ,please take a look at the Known issues in Readme file at first.

Alright, the short answer is don't use NSRunAlertPanel. That family of functions have been discouraged for some time now, and superseded by NSAlert. Use NSAlert instead.
(Unfortunately the class reference for NSRunAlertPanel etc. doesn't mention this; I'm trying to remember where it was first documented; perhaps a release note)

Related

Modal NSSavePanel disappears after animating in when begun from a completion block

G'day!
Note: Minimal example linked below. I'll refrain from longish code excerpts and rather explain the problem concisely.
I am in the process of updating an old (but small) Cocoa application to current APIs.
One of the places that looked easy enough at first: When the user tries to close the application window with unsaved changes, the app first displays an NSAlert asking "Save your stuff?". If that is confirmed a modal NSSavePanel is shown. In the original code they were opened via, respectively:
beginSheetModalForWindow:modalDelegate:didEndSelector:contextInfo:
beginSheetForDirectory:file:modalForWindow:modalDelegate:didEndSelector:contextInfo:
Current Cocoa API uses completion blocks and thus the alert prefers to be shown via beginSheetModalForWindow:completionHandler:. So I moved the code from the didEndSelector into the completionHandler.
Unfortunately the modal NSSavePanel does animate in but disappears immediately together with the application main window if it is shown from the NSAlert's completion block. If I switch the alert back to the didEndSelector I can show the NSSavePanel either selector-basedly or completion block-ly just fine.
Here's the NSAlert's completion block that forwards to the disappearing save panel.
I have thought about threading issues. All of this is happening on the main thread. Maybe there's something subtle going on with run loop modes that I'm missing?
The minimal example is available over on GitHub. You can switch between selectors and blocks with defines in AppDelegate.h. All the interesting code is in AppDelegate.m. (Unless the problem is somewhere else...)
As #Willeke pointed out this wasn't an overly mysterious issue with threading and whatnot. No. It was just me having looked at the code way too often over the course of days.
The solution is simple:
The breaks in the switch statement in confirmUnsavedChanges are missing.

How to run custom code in the main thread of Cocoa applications

I'm a windows developer and I'm having a hard time understanding the right way to run code in the
NSApplication's main thread.
Most of my code is running in a cvdisplaylink thread (it's an opengl app)
THe problem is that I can't call things like NSOpenPanel from it - it crashes the app and warns about only running stuff like this from the main thread.
It's fine, but the main thread is completely opaque as far as I understand, and I can only make it do things with events. The NSApp sendAction method sounded promising - because I could explicitly specify which method to call. But it didn't 'send' any thing, it just called this method directly from the same thread.
Am I understanding this right? Do I have to push some sort of a custom event (perhaps NSEventTypeApplicationDefined) to the main thread queue for this to work properly?
And if so, how to I respond to custom events like that?
Like this:
dispatch_async(dispatch_get_main_queue(), ^{
// do whatever
});
If what you want to do is to call a method of an Obj C object, the old school Cocoa way (which still works) is to use performSelectorOnMainThread:withObject:waitUntilDone:
E.g. to hide a window by calling its "orderOut:" method you would do this.
[theWindow performSelectorOnMainThread:#selector(orderOut:)
withObject:nil
waitUntilDone:NO];

Problem with NSProgressIndictor on mojave

I implemented NSProgressIndicator on sierra os and it worked fine in the application on it.However, for testing when i moved the application to high-sierra or mojave, theprogressbaris not displayed at all, while it works fine on sierra
when clicking the button in the application, inside the button function, i initiated the progressbar as follow,
[_progressbar startAnimation:sender];
[_progressbar setHidden:false];
and when i return i change setHidden to true. So, where am i possibly going wrong?
UPDATE:
I tried to use the following code,
//Create the block that we wish to run on a different thread.
void (^progressBlock)(void);
progressBlock = ^{
[_progressbar setDoubleValue:0.0];
[_progressbar startAnimation:sender];
[_progressbar setHidden:false];
// running = YES; // this is a instance variable
//NOTE: It is important to let all UI updates occur on the main thread,
//so we put the following UI updates on the main queue.
dispatch_async(dispatch_get_main_queue(), ^{
[_progressbar setNeedsDisplay:YES];
});
// Do some more hard work here...
}; //end of progressBlock
dispatch_queue_t queue = dispatch_get_global_queue(0,0);
dispatch_async(queue,progressBlock);
I noticed this problem on my old macOS apps, and if I recall correctly it was after first testing in High Sierra betas, as you just found.
Most user interface views need the main thread to draw or receive user input. But it seems that, in earlier macOS versions, Apple had done some magic to make NSProgressIndicator display and animate even if the main thread was busy doing other work. Then in recent macOS versions (10.13?), this magic seems to have been removed.
It never was a good idea to do long-running work on the main thread, since it makes other views in your user interface, which never had the NSProgressIndicator magic, unresponsive to the user. I think Apple figured that since we're all multi-threaders now with Dispatch (also called Grand Central Dispatch), it was time to remove the magic, which was probably difficult for them to maintain. I'd always found it to be somewhat buggy and unreliable.
So the answer is that you need to get whatever work that progress indicator is tracking off of the main thread. You will probably be using dispatch_async() and friends, although NSOperation and NSOperationQueue still work and will also do the job.

Can't insert NSAttributedString into NSTextView

Previously I've been able to use the following code to place attributed text into an NSTextView field
[[self->content textStorage] appendAttributedString:contentString];
However, upon updating to the Xcode 6 Beta to develop for Yosemetie, what used to work now gives me this error:
2014-08-26 11:06:06.635 JaxWire[59482:1765389] An uncaught exception was raised
2014-08-26 11:06:06.636 JaxWire[59482:1765389] Modifications to the layout engine must not be performed from a background thread.
and a whole much more globety gloop that I don't think has much use.
To give context as to how it works, a user enters text in a field, triggers a method called GO in the class InterfaceManager which then runs the following code to spawn a new thread ProcessQuery in the AppDelegate class, which is where I am attempting to change the content of NSTextView with an attributed string
[NSThread detachNewThreadSelector:#selector(processQuery:) toTarget:self withObject:query];
It's worth noting that this works if I use a standard string by executing [self->content setString:#"String"]; but when it comes to using an attributed string, it doesn't work.
How would I go about fixing this problem?
Thanks in advance! :)
Try editing the string on the main thread instead:
dispatch_async(dispatch_get_main_queue(), ^()
{
[[self->content textStorage] appendAttributedString:contentString];
});
it has nothing at all to do with Xcode. You are explicitly asking for this to be done on a background thread by using [NSThread detachNewThreadSelector], and as has been pointed out in comments, this is not allowed. You have never been "allowed" to modify the UI from a background thread, but often you could do so without noticeable problems. Yosemite is likely just enforcing this more strictly than previous versions of OS X.
I suppose the reason it works with setString: is because setString: probably doesn't immediately call the layout engine and rather the next drawing pass on the main thread does that.
you either have to not detach processQuery to a background thread, or have processQuery call appendAttributedString on the main thread using something like:
[[self->content textStorage] performSelector:#selector(appendAttributedString:) withObject: contentString]
or using GCD dispatch_async() with the main thread's queue dispatch_get_main_queue()

Cocoa program exits with no crash

I managed to create a bug where a cocoa program is exiting.
-[NSApplication terminate:] is apparently not called.
Nor is -[NSException init].
There is no crash. What I am seeing in gdb is that exit() is being called directly from NSApplicationMain without going through the normal [NSApplication terminate] path.
Any ideas about how to debug this and/or how this (seemingly "impossible" behavior) could happen?
Thanks in advance from a cocoa newb.
I would start with judicious use of breakpoints. Narrow it down to the last place where your code is seen before the faux crash and then start looking around that area for memory issues. Whenever I have really strange behavior, it's almost entirely due to a strange memory bug.
There are few remaining routes that would cause your application to voluntarily exit without going through the normal [NSApplication terminate] path.
Try setting a breakpoint on "stop:". I suspect somehow, perhaps an inappropriately named performSelector call? Or a badly named IB connection, you are calling this method, which is documented to exit from the main event loop.
After mulling this over I guessed that I was doing something wrong "in the big picture". So, "in the big picture", what I was doing is calling a modal dialog (by loading a window nib, showing the window and then using [NSApplication beginModalSessionForWindow]...[NSApp runModalSession]... and upon close [NSApp endModalSession]. This has worked for me in other chunks of code no big deal. The critical difference was that this modal window was being loaded and run out of another class's awakeFromNib.
I wrote a simple app that will reproduce this behavior. So what happens is that the modal dialog loads happily and when you close the modal window not only does it close but the app exits. I imagine that this is an "as designed" behavior. I was reading up in apple docs to see if I could understand why this was so but I am will muddled on it. I surmise that somehow my dialog's modal event loop has hijacked the app's event loop so that when the dialog ends so does the app.
If anybody has a better understanding of this mistake please feel free to enlighten us...
Thanks to all for reading and suggestions.
PS -- I changed my code to use [NSApplication runModalForWindow:] instead of the modal session calls and all is well (I think). (Using the modal session would allow me to launch a web browser from a modal dialog -- while runModalForWindow does not -- but in this case I don't need that functionality.)