tags:

views:

3269

answers:

6

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"]];
}
A: 

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!

A: 

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.

Patrick Burleson
+2  A: 

Hi Patrick,

Did you set up an animation context (UIView class method does that) around this for loop? Without it changes are immediate instead of animated.

Bill Dudney
Bill, I tried surrounding the loop with [UIView beginAnimations:@"animation" context:nil];and [UIView commitAnimations];But it had not effect. All I see is a huge pause in the UI while the loop goes and then "bang", just the last image is "on".
Patrick Burleson
hm, imageview must not animated its 'image' property i guess, bummer. You could consider using layers here as they do animate the change in contents (with a cross fade by default). Or you could do the 'recommended' view thing and add a 'superview' that would contain either the on or off image view
Bill Dudney
cool Bill Dudney is on SO now. welcome aboard I really like your book about iPhone dev.
François P.
A: 

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.

Brad Larson
A: 

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 )

Patrick Burleson
Calling drawing code is only safe when called from the main thread. I'm surprised it worked at all :)
rpetrich
+1  A: 

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

Roger Nolan
Calling the swapping on a background thread to do the swap on the Main Thread and then sleeping allows the update. I'm going to try using UIView's around the UIImages then make a CA block as well. UIIMageView apparently doesn't expose swapping of image to CA.
Patrick Burleson