Why do my interface objects respond out of order? - objective-c

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.

Related

How do I notify incoming scene when presentScene:transition: is complete?

Let's say I'm transitioning from an old SKScene of an SKView to a new one:
//In some controller somewhere...
-(void)swapScenes{
MySceneSubclass *newScene = [[MySceneSubclass alloc] initWithSize:CGSizeMake(1024, 768)];
SKTransition *transition = [SKTransition crossFadeWithDuration:REALLY_LONG_DURATION];
[[self mySKView] presentScene:newScene transition:transition];
}
Let's additionally say I want my new scene to perform some action or animation once the transition is completed, but not before. What would I use to trigger that?
At first I thought I'd be able to override didMoveToView: to do this, but it turns out this is called at the very begining of the transition (in hindsight, this makes sense. In a crossfade, the incoming scene is composited at the very beginning of the animation, even if its opacity is very low).
Next, as a hail mary, I tried inserting a call to the new scene right after presentScene:
-(void)swapScenes{
MySceneSubclass *newScene = [[MySceneSubclass alloc] initWithSize:CGSizeMake(1024, 768)];
SKTransition *transition = [SKTransition crossFadeWithDuration:REALLY_LONG_DURATION];
[[self mySKView] presentScene:newScene transition:transition];
[newScene doSomethingAfterTransition]; //<----
}
But presentScene: predictably returned immediately causing this method to be called long before the transition had completed.
As a last resort, I'm considering something like:
[newScene performSelector:#selector(doSomethingAfterTransition) afterDelay:REALLY_LONG_DURATION];
But I'd really like to avoid that if at all possible. It seems like there ought to be an delegate action or notification or something that knows when the transition is over, right?
The answer to this was staring me in the face. As I mentioned above, in a transition both scenes need to be present throughout the animation. Thus the incoming scene's didMoveToView: is called immediately at the beginning of the transition instead of at the end as I expected.
Of course, by this same logic, the outgoing scene's willMoveFromView: won't get called until the end of the transition. Which is what I was looking for in the first place.
So, you can override -willMoveFromView: of the outgoing scene (or, more likely, some shared superclass) to send a notification or call a delegate or whatever you like when transition completes. In my case, I have it call a block so I can keep everything local to my -swapScenes method, but YMMV.
Perform selector after delay with the same delay as the transition is perfectly reasonable.
This is a commonly desired need and (ideally) we shouldn't be programming around the tools. The two workaround provided by #jlemmons and #LearnCocos2D are both functional, but each have their fallbacks.
I would highly suggest going to the Apple Bug Reporter and requesting the addition of
- (void)[SKScene didFinishTransitionToView:(SKView *)view]
The more people request, the sooner it may appear.

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;
}

Call method repeatedly after time delay until end condition reached

I have a crosshair on the iphone screen and when a pavilion (dot on map) moves under it i want to start zooming on the map.
I all ready got the detection to work when a pavilion comes under the crosshair and the moment it is not under it anymore.
For now i post it in here in psuedo code:
- (void) checkForPavilionUnderCrosshair {
if(new pavilion under crosshair) {
// start zooming
}
else if(just left pavilion under crosshair){
// stop zooming
}
}
So what i need to do now is keep triggering:
mapView setZoom:(mapView.zoom+0.1) animated:NO];
And being able to stop that progress when the crosshair moves off the pavilion.
I did some searches on stackoverflow but the posts i found did not include stopping it for example. I have no experience with timing things in programs so could someone help me a litle by telling what i'm looking for?
I tried
[self performSelector:#selector(zoomTest) withObject:self afterDelay:0.0];
If i keep touching the map and move my finger then it keeps checkForPavilionUnderCrosshair just like i want.
But the perform selector get's fired after i stop touching the screen, so if i touch the screen for 20 seconds it fires 20 seconds to late.
Any workaround for that?
You can call [self performSelector:#selector(checkForPavilionUnderCrosshair) withObject:nil afterDelay:1.0] at the end of your method to have it called again after the specified time period. Note that the delay is in seconds.
If you want something more advanced, you can look at NSTimer over at the Apple docs, or see this SO question for an example and explanation.
If you decide to use the simple performSelector method, you can simply not call the method when you don't want to repeat anymore. If you choose to use NSTimer, call invalidate on the timer to stop it.

Update screen in Cocoa/Objective C following button press

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.

Swapping UIImageView Images in a loop

I have a NSArray of UIImageViews that I want to loop over and quickly swap out an "on" and "off" state. I wrote the code to do so in a for loop instead a method that was called when the user tapped a UIButton ( the button's action ).
Here's that loop:
for(int i = 0; i < [Images count]; i++) {
if( i > 0 ){
[self toggleImageViewOff:[Images objectAtIndex:i - 1]];
}
[self toggleImageViewOn:[Images objectAtIndex:i]];
[NSThread sleepForTimeInterval:0.5f];
}
The UI did not update as I expected as I only ever saw the last UIImageView in the "on" state. I figured that the drawing update of the views must occur in the main thread this code was also executing in. So I learned about performSelectorInBackground:withObject: . Performing the toggleImageViewOn/Off methods using this made the loop work. The problem is if I make the sleep interval too short I can have an "on" update after an "off" with Threads operating out of order.
So I had the bright idea of moving the whole loop with the sleep into its own method and calling that from the action method using performSelectorInBackground:withObject: . I tried that and I'm back to not getting an updated view until the loop is over.
That's a long winded way to get to my question:
What's the best way to animate this to guarantee the on/off code fires in the right order and still get view updates, even at high speeds? ( i.e. looping very quickly )
I tried to think about how I'd do it with CoreAnimation, but I can't seem to get my head around how to do it there.
For bonus, here are the toggle methods:
- (void)toggleImageViewOn:(UIImageView *)theImageView {
[theImageView setImage:[UIImage imageNamed:#"on.png"]];
}
- (void)toggleImageViewOff:(UIImageView *)theImageView {
[theImageView setImage:[UIImage imageNamed:#"off.png"]];
}
Did you set up an animation context (UIView class method does that) around this for loop? Without it changes are immediate instead of animated.
The problem is that you are not giving any of the UIImages time to draw. The drawing code is optimised to only draw what's needed - rendering all those intermediate stages is optimised out.
Sleeping the main thread doesn't actually give it chance to run.
Bill is right in that you need to set up an animation context around your loop. This will capture all of the UIView changes you make and then play them out. The easiest way to do this is using Core Animation. Core animation 'records' changes in UIElemenets and plays them back. Your code (without the sleep) will work just fine in a Core Animation block.
Apple have a reasonable cookbook for Core Animation on their site
You're on the right track with moving the loop to a background thread, but you also need to make sure that you give the main run loop a chance to update the UI. You should be able to replace the direct calls to toggleImageViewOn: and toggleImageViewOff: with something like
[self performSelectorOnMainThread:#selector(toggleImageViewOn:) withObject:[Images objectAtIndex:i] waitUntilDone:NO];
This will do the UI update on the main thread, and by not waiting until the update is done you give the main run loop a chance to reach its end. You run into the same issue with things like progress bars, where they won't change until the loop ends unless you do your updates from a background thread with a UI update call like the one above.
Hey Patrick. Have a look at UIImageView's animationImages property, as well as the animationRepeatCount and animationDuration properties. If you put your on/off images into an array and assign that as the animationImages property, you should be able to control the repeat and duration to get the desired effect.
Hope that helps!
Thanks for that. I've already looked into UIIMageView's animationImages property. That's not exactly what I'm attempting to do. I'm cycling between several UIImageView's that are placed near each other to give the impression that a light is moving between them and cycling over them. So an individual UIImageView's animation is separate from each other as I need to swap the image as necessary in code.
Calling peformSelectorOnMainThread:withObject:waitUntilDone: from the loop on the background thread does indeed update the view as quickly as I can think I will ever need. I'm curious why I need to do the UIImageView swap on the Main thread? Why wouldn't changing it on the background thread and then using NSThread's sleepForTimeInterval allow the main thread to update the drawing anyway?
I guess I need to go read up on the run loop and where drawing updates occur.
Thanks so much for the help. ( I'm also going to try some additional suggestions from Bill Dudney, that I think will work based on CoreAnimation )