Newbie Objective C/Cocoa question: I have an application with some data entry fields and a "do it" button. When the button is pressed, some computation takes place and output data is displayed in a table view and some text fields in the same window. What I'd like is that when the button is pressed that the text fields and the table view are both cleared while the computation takes place.
I've tried making the appropriate calls as the first few statements of the action routine for the button press, but that doesn't work. I would imagine that the runtimes don't get called to do the screen update until after my action routine is finished.
Is there a simple way to do what I want to do? Thanks.
You imagine correctly.
The usual way to do this sort of thing is to use NSObject's performSelectorInBackground:withObject: to start the heavy calculation in the background. Then once the background code finishes doing its work, use performSelectorOnMainThread:withObject:waitUntilDone: to call another selector on the main thread to update the UI (remember, UI calls may only be done from the main thread).
You're correct about the screen updates not taking place until after your routine finishes. Most drawing to the screen is queued to improve performance.
When you change the value in an NSTextField, it knows to call [self setNeedsDisplay:YES] in order to queue its need for redrawing. If you want to force it to display, you can call [textField display]. (Note that calling [textField setNeedsDisplay:YES] will not cause immediate display). Things get a bit more difficult with an NSTableView, as this -display method is unlikely to work for it.
While you could create a secondary thread to do your processing, that would create a lot of complexity that may not be worth it. You might consider using -performSelector:withObject:afterDelay: to begin your processing routine rather than calling it directly.
- (IBAction)buttonClicked:(id)sender {
[textField setStringValue:#""];
[tableView reloadData];
// instead of doing the following:
// [self processData:nil];
// do
[self performSelector:#selector(processData:) withObject:nil afterDelay:0.0];
}
- (void)processData:(id)sender {
// process the data
[textField setStringValue:#"the results"];
[tableView reloadData];
}
Using -performSelector:withObject:afterDelay: is different than calling the method directly, as it causes the method to be called not immediately, but scheduled to be called "ASAP". In many cases, your app will be able to squeeze in the updates to the UI before it can get to performing that computation method. If testing reveals this to be the case, then you can avoid having to go to the trouble of creating a secondary thread to do the processing.
If you want to force updating screen then call setNeedsDisplay from your UIView.
I would imagine that the runtimes
don't get called to do the screen
update until after my action routine
is finished.
Bingo. Your button's action method is called on the main thread, which is the same thread that is responsible for updating the user-interface. So the interface will not update until after your action method returns.
To get around this, you can split your action method into two parts. The first part makes the calls to clear your previous view and set whatever new state you want to use for rendering. The second part does the new calculations, and is moved to its own method. Then, at the end of the first part, add something roughly like:
[self performSelectorInBackground:#selector(myActionSecondPart) withObject:nil];
...to run the computation part in the background. Then your UI will update while the computation runs.
Related
I have a Cocoa app that draws a lot data to the main screen(31000 samples by about 315 channels) so we are being very studious about profiling and getting everything as efficient as possible. I have a window controller that when opened updates it's view every 2 seconds based on the data. I am using an NSTimer and specifying the view update method.
The problem I am having is every time the timer fires the method, the main display hiccups slightly. I thought it would just be a matter of optimizing the drawRect method in the view subclass, but when I could not find any specific area in the draw rect method where the performance was bad, I decided to try commenting out the contents of the drawRect method.
results:
If I comment out the contents of the drawRect method, I will still get a hiccup.
If I comment out the call to [view setNeedsDisply: YES] in the calling method, it resolves the hiccup.
What Ive Tried:
1) I modified the method calls so that when the timer fired I was using performSelectorOnMainThread to call the view
2) I then tried to use the main dispatch queue with async.
neither of these things worked.
There is some kind of lag happening here even when there is no drawing work to do.
Any help is appreciated.
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;
}
I have an NSView in a Mac OS X application that draws itself. When I am ready for this to happen I request it with the call:
[self.imageRenderedView setNeedsDisplay:YES];
My question is, does this call block? That is, is it synchronous, and can I assume the drawing has happened by the time the subsequent statement is executed? This assumption seems to work for me, but I feel a bit insecure about it.
The setNeedsDisplay: call only marks the view as needing display by setting
a flag in the view object.
Therefore it returns very quickly, but the drawing has not yet happened when the method returns.
From the documentation:
Whenever the data or state used for drawing a view object changes, the
view should be sent a setNeedsDisplay: message. NSView objects marked
as needing display are automatically redisplayed on each pass through
the application’s event loop. (View objects that need to redisplay
before the event loop comes around can of course immediately be sent
the appropriate display... method.)
I'm working on a card game and trying to get cards to deal one after the other. I have a method that animated a card from the deck to a player, and in viewDidLoad I call this method four times. The problem is all four cards get dealt simultaneously. How do I stop a method in its tracks for a period of time?
I know that the scheduledTimerWithTimeInterval method calls another method after a delay, but I'm looking for a way to interrupt the current method after calling the deal method once and then continuing with the rest of the current method. sleep() also doesn't work. I tried putting it between calls to the deal method, but it just executed all the sleep()s and then did all the animation at once again. Any help is much appreciated. Thanks!
You are going down the wrong road. Attempting to sleep a method is not the way to approach this. You want to break the task into steps to be performed serially and perform each step only after the previous step is completed.
Say you have a variable called 'cardCounter' and one called 'cardMax'. Then you have a method called 'dealCard'. In viewDidAppear you intialize 'cardCounter` to zero and 'cardMax' to 4 (or however many cards are to be dealt. Then you call the 'dealCard' method.
(actually, you probably want a method called newGame or something since you will likely want to have multiple games and you don't want to tie your game setup to the viewDidAppear event. So in viewDidAppear you would call 'newGame' and do your initialization there.)
- (void)dealCard {
cardCounter++;
if (cardCounter > cardMax){
// all cards are dealt
// call some method to start game
// or do any other set up;
} else {
// call some method to animate the card
// using core animation with a completion handler?
// using a ^block with a completion handler?
// either way, in the completion handler call
// 'dealCard' again
}
A hint. Create a setup method which is called from your init only once! In that setup method you can work with `performSelector" as tomi said. The Selector is "your" method that moves a card from the deck to the player.
(void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
I have an IBAction for when a button is clicked:
- (IBAction)importButtonClicked:(id)sender
And I want a series of events to take place like:
[_progressLabel becomeFirstResponder]; // I tried this but to no effect
_progressLabel.stringValue = BEGIN_IMPORT_STRING;
[_importButton setEnabled:FALSE];
_fileField.stringValue = #"";
[_progressIndicator startAnimation:nil];
But what ends up happening is the _progressIndicator animation takes place before the _progressLabel text appears. And often times the text won't appear untili the _progressIndicator animation has stopped. How do I fix that?
Put the work you're doing which takes time (I assume that's what the progress indicator is for) on a separate thread. You don't have to do this manually in Cocoa, but instead, use Grand Central Dispatch (GCD), NSOperationQueue or such a construct available. You'll find lots of resources on GCD.