Problem with NSProgressIndictor on mojave - objective-c

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.

Related

dispatch_async doesn't work as advertised

I'm still pretty green with iOS, but the code below seems like it should work. I have a bunch of work to do in the background so a "busy indicator" is supposed to be shown to the user while the work is done. But what happens is the busy indicator does not appear for several seconds. It only appears once most of the work (but not all) has completed.
- (void) fetchDataTest {
// next line will create and display a busy indicator
__block MRProgressOverlayView *overlay = [MRProgressOverlayView showOverlayAddedTo:self.view title:#"Loading…" mode:MRProgressOverlayViewModeIndeterminate animated:YES];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^(void) {
[self fetchData];
dispatch_async(dispatch_get_main_queue(), ^(void) {
[overlay hide:YES];
});
});
}
I have lots of logging (not shown above) that runs so slowly I can watch the output log (including thread ids) and see overlay has been created and background work started - yet no overlay on my device appears for several seconds.
How can I get the overlay to appear immediately?
Observations:
you shouldn't use GCD to make synchronous data fetches look asynchronous — just use the asynchronous fetches directly. They're much better for overall scheduling and for battery life;
it's the bit that occurs before the dispatch_asyncs that isn't happening, not the stuff within them;
Jack Wu's guess is probably right: on what thread or queue is fetchDataTest being performed? It should be the main queue.
On the last point: UIKit is not generally thread safe. With a few exceptions, you should always use it from the main queue. That's a large part of what the main queue is for nowadays: all user interaction. Then heavy work should be passed off elsewhere.

How can I allow a user adjust an NSSlider without pausing the application update loop?

NOTE: Updated below...
I have a cocoa desktop application which consists of a series of controls around a custom NSView. I am using displayLink to drive the updates.
When a user clicks on an NSControl (a slider, a button, a checkbox, a radio button) the application appears to freeze until the mouse is released. I can confirm in fact that the displayLink callback (getFrameForTime) is NOT firing during the time. If I create a timer, that also does not fire, both remain paused until the user releases the mouse, at which point the application resumes updating.
The control is bound, and if I update that value from another thread (for example, via a callback from a MIDI interface) the slider behaves as expected: it moves, the value updates and the application does not pause.
I feel like this should be a fairly obvious fix, but I'm stumped.
Checking "continuous" in IB does as advertised: sends the values continuously, but still exhibits this behavior (preventing the UI update) until the mouse is released.
This seems to be related specifically to mouseDown on NSControl? Why would this block, and do I really need to subclass all my UI elements to change this behavior (seems extreme)
DisplayLink is in its own thread, so why mouseDown on the main thread block it? If this is the case, given the injunction on updating the Cocoa UI from other than the main thread, how do I deal with it?
Any help much appreciated.
Update
Per #Nikolai's comments below, I can confirm that using an NSTimer and adding it to NSEventTrackingRunLoopMode does NOT block. However, I would really like to use CVDisplayLink which (according to the documentation) runs in it's own thread and should not be blocked in this way. Unlike CADisplayLink, I cannot find a way to explicitly assign a runloop to CVDisplayLink (it seems it doesn't work that way), so perhaps the new question should be:
Why does CVDisplayLink block on NSEventTrackingRunLoopMode?
When clicking on an NSControl the runloop mode goes from NSDefaultRunLoopMode to NSEventTrackingRunLoopMode, as long as the mouse is down. That means that only run loop sources (display link) and timers fire that have been added to this mode.
You can add timers to any mode by using -[NSRunLoop addTimer:forMode:]. For a display link the equivalent method is -[CADisplayLink addToRunLoop:forMode:].
To make your animation continue during event tracking you would do something like:
[myDisplayLink addToRunLoop:[NSRunLoop currentRunLoop]
forMode:NSEventTrackingRunLoopMode];
Your test project shows that you are calling a view's display method from within the display link's callback.
When commenting the display message out, the display link is called continuously even while moving the slider.
So what goes wrong is that when the runloop goes into event tracking mode, the call to display on the display link's thread blocks until the mouse is released and the run loop goes back to default mode. You can easily confirm this by putting a log statement before the call and one after it.
Why exactly that happens is not clear to me. What is clear is that it's illegal to call a view's methods from a background thread. You have to trigger the view's display by dispatching a setNeedsDisplay: on the main thread:
static CVReturn MyDisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp* now, const CVTimeStamp* outputTime, CVOptionFlags flagsIn, CVOptionFlags* flagsOut, void* displayLinkContext)
{
dispatch_async(dispatch_get_main_queue(), ^{
[(__bridge MyCustomView*)displayLinkContext setNeedsDisplay:YES];
});
return kCVReturnSuccess;
}

NSRunAlertPanel caused performance issue on multithreading

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)

AFNetworking Set runLoopModes?

AFNetworking default runLoopModes is NSRunLoopCommonModes.I want use NSDefaultRunLoopMode,and set it
operation.runLoopModes = [NSSet setWithObject:NSDefaultRunLoopMode];
But it doesn't work. when I scroll the scrollView,download task still running.
Anybody can help me? thanks.
So here we go, alternative solution, without hardcoded injection. Assuming you are using UIImageView+AFNetworking extension:
// user began scrolling
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
[[UIImageView af_sharedImageRequestOperationQueue] setSuspended:YES];
}
// user released finger and scrolling animation is finished
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[[UIImageView af_sharedImageRequestOperationQueue] setSuspended:NO];
}
Also check out Foursquare's solution, which relies on the same technique.
Recently I encountered similar question, while I was implementing images lazy loading in UITableView. User scrolling should have higher priority here.
Your direction is correct, the problem is in runloop modes. With UIImageView+AFNetworking extension, images are loaded with AFURLConnectionOperation, which is working on a separate network thread (see implementation), so NSURLConnections are running not on main thread.
To make the tasks stop when UITrackingRunLoopMode is active, you need to move networking code on main thread somehow.
Possible solutions:
Setup default runloop mode for your request operation.
AFImageRequestOperation *requestOperation = [[AFImageRequestOperation alloc] initWithRequest:urlRequest];
[requestOperation setRunLoopModes:[NSSet setWithObject:NSDefaultRunLoopMode]];
Put connection on main thread (that's ok, NSURLConnections are running their own threads): Find + (NSThread *)networkRequestThread and make it return [NSThread mainThread]. That might be not suitable with other situations, beware locks.
I am still not sure why AFNetworking is creating it's separate network thread. To process incoming data in background? If anybody have a guess, please reply.
Also see this question.

iPad app freezes during NSOperationQueue

I have a problem with an app where I implemented an NSOperationQueue.
It seems that pressing the close button of the iPad makes the UI freeze. The app itself is still running in the background, the UI is updated, it's just that it won't answer to touches or rotation anymore and it doesn't get closed as it should be.
I have an update module that downloads quite a long list of xml files and saves them on the device. The operation queue has a MaxConcurentOperations value of 2.
Usually everything works fine, the app runs fine and dandy, responding to my touches and rotation UNTIL I press the device's button. After this, the UI simply freezes. The progress is still updated (an UILabel), recurrent animations are still displayed, but the app is not closed until all the operations are done.
I'm not calling waitUntilAllOperationsAreFinished on my queue so I don't know what might be causing this.. So far, I've only made tests on a first generation iPad, with iOs 5.0
If anyone can provide me with some tips, I'll really appreciate it. If necessary, I can post the NSOperationQueue and NSOperation class codes, but somehow I have the feeling this about me approaching this wrongly, and not about a faulty line of code
[edit]
I also use a timer to periodically check on the download status, but I noticed that not calling the timer doesn't eliminate the problem
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(xmlDownloaded) userInfo:nil repeats:YES];
[edit2]
After some further research, I made sure that my operations are concurrent and just to be sure, I changed the way I added my operations to the queue.
Instead of
[downloadQueue addOperation:op];
I added them to a mutable array called "operations" and in the end I used
[downloadQueue addOperations:operations waitUntilFinished:NO];
but my app still freezes when I press the close button...
Wild guess, you are locking the main thread waiting for your operation to complete / you are destroying your operation delegate on viewWillDisappear?
It seems there was another function causing this, even though I don't understand why...
I had a label animation
CABasicAnimation *fadey = [CABasicAnimation animationWithKeyPath:#"opacity"];
[fadey setToValue:[NSNumber numberWithFloat:0.35f]];
fadey.repeatCount = HUGE_VALF;
[fadey setAutoreverses:YES];
[fadey setDuration:0.6f];
This small function was causing the app to wait until all the operations were finished in order to close the app. The weird part is that the function was not even called in with the NSOperations, but before them
Again, I have no idea why... everything breaks when I press the close button, otherwise there are no problem. So if anyone else runs into a similar issue, it might help to check for some repeating animations