Swapping UIImageView Images in a loop - cocoa-touch

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 )

Related

Loop problems in Objective-C

I have two problems both with loops.
First question, is it possible to make loop(while or something else) that would wait my command.
For example, I need to choose a possible move and until I have chance for next move I must have chance to choose.
While(eat==1){
if(CGRectContainsPoint([BOX9 frame], [touch locationInView:self.view])){
eat=0;
}
}
My problem is that while spinning in circle and BOX9 can't be pressed because spinning and spinning. I need some loop that spin circle but will allow selection of next command. I hope I was clear, English is not my strong point.
Second problem
Second problem is with while loop. On touch I show image from UIimageView controller to another. Sometimes I have more movements, and I need use while because i never know how movements I wold have. And I would like to make this movements slower.
If i write in code that i want uiimage1.image=someimage; that work fine. But problem is when I say for example:
uiimage1.image=someimage;
while(eat==1){
uiimage2.image=uiimage1.image;
uiimage1.image=NULL;
uiimage3.image=uiimage1.image;
}
(This is only example code,that I wrote now,to better explain problem.)
If i making some waiting inside "while loop", always I getting that slower show is of only first image,second two changes are fast.
I try with transition effect to make slower, but that works only when I load some picture in imagecontroller with touch. If i wrote in code, transition wont work.
All is in touchesBegin function.
Thank's for help.
So, from what I can tell, you are making a game, and want to animate things. Your problem is that your while loops are on the main thread, so until they finish no user input is accepted and the screen doesn't refresh.
There are several ways to solve this, all of which involve moving the execution of things that happen in "real-time," by which I mean things that are presented to the user, off of the main thread.
The first: You make an NSTimer with scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:. Your selector handles the animation of things in "real time". When touchesBegan is called, you set the flag eating. This looks roughly like this in code
-(void) setUp
{
NSTimer scheduleTimerWithTimeInterval: 1.0
target: self
selector: #selector(runLoop)
userInfo: NULL
repeats: YES];
}
-(void) runLoop
{
if(eatingDuration && eating)
{
eatingDuration--;
//do something because we're eating
}
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//if we need to set eating to yes do it
eating = YES;
eatingDuration = whatEverWeNeedItToBe;
}
This might be thought of as a "run loop," because it is called repeatedly until you get rid of the timer. It is probably the best method to do this.
The second: You use the animateWithDuration:animations: class method of UIView to animate each individual element.

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.

Calling -setNeedsDisplay:YES from within -drawRect?

I am customizing my drawRect: method, which serves to draw a NSImage if it has been "loaded" (loading taking a few seconds worth of time because I'm grabbing it from a WebView), and putting off drawing the image till later if the image has not yet been loaded.
- (void)drawRect:(NSRect)dirtyRect
{
NSImage *imageToDraw = [self cachedImage];
if (imageToDraw != nil) {
[imageToDraw drawInRect:dirtyRect fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 respectFlipped:YES hints:nil];
} else {
//I need help here
[self setNeedsDisplay:YES];
}
}
My question is how to do the latter. [self cachedImage] returns nil if the image is unavailable, but anytime within the next few seconds it may become available and at that time I want to draw it because the custom view is already on screen.
My initial instinct was to try calling [self setNeedsDisplay:YES]; if the image wasn't available, in hopes that it would tell Cocoa to call drawRect again the next time around (and again and again and again until the image is drawn), but that doesn't work.
Any pointers as to where I can go from here?
EDIT:
I am very much aware of the delegate methods for WebView that fire when the loadRequest has been completely processed. Using these, however, will be very difficult due to the structure of the rest of the application, but I think I will try to somehow use them now given the current answers. (also note that my drawRect: method is relatively light weight, there being nothing except the code I already have above.)
I currently have about 10+ custom views each with custom data asking the same WebView to generate images for each of them. At the same time, I am grabbing the image from an NSCache (using an identifier corresponding to each custom view) and creating it if it doesn't exist or needs to be updated, and returning nil if it is not yet available. Hence, it's not as easy as calling [view setNeedsDisplay:YES] from - (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame or another method.
My initial instinct was to try calling [self setNeedsDisplay:YES]; if the image wasn't available, in hopes that it would tell Cocoa to call drawRect again the next time around (and again and again and again until the image is drawn)
This would be incredibly inefficient, even if it worked.
anytime within the next few seconds it may become available and at that time I want to draw it
So, when that happens, call [view setNeedsDisplay:YES].
If you have no means of directly determining when the image becomes available, you'll have to poll. Set up a repeating NSTimer with an interval of something reasonable -- say 0.25 second or so. (This is also pretty inefficient, but at least it's running only 4 times per second instead of 60 or worse. It's a tradeoff between two factors: how much CPU and battery power you want to use, and how long the delay is between the time the image becomes available and the time you show it.)
my drawRect: method is relatively light weight, there being nothing except the code I already have above.
Even if you do nothing at all in -drawRect:, Cocoa still needs to do a lot of work behind the scenes -- it needs to manage dirty rects, clear the appropriate area of the window's backing store, flush it to the screen, etc. None of that is free.
Well, usually there is some delegate method that is called, when a download of something finishes. You should implement that method and call setNeedsDisplay:YES there.
The documentation for webkit:
https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/DisplayWebContent/Tasks/ResourceLoading.html#//apple_ref/doc/uid/20002028-CJBEHAAG
You have to implement the following method in your webview delegate:
- webView:resource:didFinishLoadingFromDataSource:
There you can call [view setNeedsDisplay:Yes]

Why do my interface objects respond out of order?

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.

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.