Monday, January 3, 2011

How To Use UIView Animation Tutorial

Use animation to see what's inside this picnic basket!
Use animation to see what's inside this picnic basket!
One of the coolest things about iPhone apps is how animated many of them are. You can have views fly across the screen, fade in or fade, out, rotate around, and much more!
Not only does this look cool, but animations are good indicators that something is going on that a user should pay attention to, such as more info becoming available.
The best part about animation on the iPhone is that it is incredibly easy to implement programmatically! It’s literally just a couple lines of code and you’re up and running.
In this tutorial, you’ll get a chance to go hands-on with UIView animation to create a neat little app about going on a picnic. The picnic basket opens in a neat animated way, and then you get to look what’s inside – and take decisive action!
In the process, you’ll learn how to use UIView animations in both the standard way and the post iOS4 manner, learn how to chain UIView animations, and learn how to tell the position of views while animations are running.
So grab your picnic basket and let’s get started!

Introduction to UIView Animation

Just so you can appreciate how nice and easy UIView animation is, consider what you’d have to do to animate a view moving across the screen if iOS didn’t provide you with built-in animation support:
  • Schedule a method to be called in your app every frame.
  • Every frame, calculate the new position of the view, based on the desired final destination, the time to run the animation, and the time run so far.
  • Update the view’s position to the calculated position.
That’s not a ton of work, but it’s annoying enough that it might make you think twice about implementing an animation. Plus, it gets a lot more complicated to keep track of the more animations you do.
But don’t worry – animations are extremely easy to use in iOS! There are certain properties on views, such as the view’s frame (its size and position), alpha (transparency), and transform (scale/rotation/etc.) which have built-in animation support. So instead of having to do all of the above, you simply:
  • Set up an animation, specifying how long it should run and a few other optional parameters.
  • Set an animatable property on a view, such as its frame, and start the animation running.
  • That’s it – UIKit will take over handle the calculations and updates for you!
So let’s dive in and see what this looks like in code, by creating an animation of a picnic basket opening when you start up the app.

An Opening Picnic Basket

Go to “File\New Project…”, choose iOS\Application\View-based Application, and click “Choose…”. Name the project Picnic, and click Save.
Next, download a copy of some images and sounds made by my lovely wife that you’ll need for this project. Unzip the file and drag the files to the Resources folder of your project. Verify that “Copy items into destination group’s folder (if needed”) is checked, and click Add.
Then double click on Resources\PicnicViewController.xib to bring up Interface Builder. Drag two UIImageViews to the view controller, one on top filling up about half the space, and one on the bottom filling up the bottom half. Set the top image view to door_top.png (and View\Mode to Top), and the bottom image to door_bottom.png (and View\Mode to Bottom), and resize the image views until they look OK, as shown below.
Basket View
Now open PicnicViewController.h and add two properties for the image views:
@property (assign) IBOutlet UIImageView *basketTop;
@property (assign) IBOutlet UIImageView *basketBottom;
You may be wondering why you didn’t create instance variables for these properties. Well, XCode has a relatively new feature where if you create a property, it will automatically create an instance variable backing the property. Cool, huh?
Also note that the properties are marked as assign here, just to make things easier for us (i.e. no need to release the instance vars since the class won’t be retaining them).
Now save the header and go back to PicnicViewController.xib and control-click on “File’s Owner” to bring up the menu. Control-drag on the circle next to basketBottom and connect it to the bottom image view, and control-drag on the circle next to basketTop and connect it to the top image view.
Now that you’ve connected the views you’ve created in Interface Builder to properties, you can animate them to open the basket when the view first appears. Switch to PicnicViewController.m and make the following modifications:
// At top, under @implementation
@synthesize basketTop;
@synthesize basketBottom;
 
// Uncomment viewDidLoad and add the following inside
CGRect basketTopFrame = basketTop.frame;
basketTopFrame.origin.y = -basketTopFrame.size.height;
 
CGRect basketBottomFrame = basketBottom.frame;
basketBottomFrame.origin.y = self.view.bounds.size.height;
 
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.5];
[UIView setAnimationDelay:1.0];
[UIView setAnimationCurve:UIViewAnimationCurveEaseOut];
 
basketTop.frame = basketTopFrame;
basketBottom.frame = basketBottomFrame;
 
[UIView commitAnimations];
Here you first calculate where to move the top and bottom images to – basically you move them just offscreen.
Then comes the fun part – the animation code! It uses beginAnimations:context to start an animation block, and then sets three parameters. The animation is set with a duration of 0.5 seconds, and not to start until 1 second in (so you can enjoy the pretty basket for a second), and the animation curve is set to ease out (so the animation goes a little bit slower at the end).
Then the frames of the two image views are set to their final destinations, and you call commitAnimations to start the animations. UIKit takes over from there and runs a neat animation of your basket opening up.
Compile and run the code to try it out for yourself – pretty easy to get such a neat effect, eh?
Basket Halfway Open

Alternate method for iOS 4+

The animation code as-written above is pretty easy, but there’s an even easier method on iOS 4. In iOS 4, UIView has some new methods you can use that use blocks:
  • animateWithDuration:animations:
  • animateWithDuration:animations:completion:
  • animateWithDuration:delay:options:animations:completion:
These methods let you put all of the parameters that you would have had to write multiple lines for in a single call. And the best part is it makes it really easy to call a block of code when the animation finishes too, via the completion parameter.
By the way, don’t be scared of blocks! Blocks have a funky syntax to get used to at first, but they can really help make your code a lot more terse and easier to read, and help keep related blocks of code closer together. Try them out and you’ll get used to them in no time!
So let’s give this a try. Go ahead and replace the code starting at [UIView beginAnimations…] and ending with [UIView commitAnimations] with the following alternative method:
[UIView animateWithDuration:0.5
    delay:1.0
    options: UIViewAnimationCurveEaseOut
    animations:^{
        basketTop.frame = basketTopFrame;
        basketBottom.frame = basketBottomFrame;
    } 
    completion:^(BOOL finished){
        NSLog(@"Done!");
    }];
This uses one of the new methods, specifying a block for the animations to run, and a block of code to be called when the animations complete. This does exactly what the code used to do, except it’s more terse, and we have an easy way to know when the animations are done.
So compile and run the code, and you should see the basket slide open as usual, but also see a console message when the basket is fully opened!
By the way – if you want to use the pre-iOS4 method and get notice when the animation completes, you can do that with the setAnimationDidStopSelector method.

A Second Layer of Fun

When you go out on a picnic, you usually don’t just throw your food straight into the basket – instead you put a little napkin on top to shield the food from pesky infiltrators. So why not have a little more fun with animations and add a napkin layer to open as well?
Go back to Interface Builder, and note that since the Image Views are taking up the whole screen, it’s going to be hard to keep track of things. To deal with this, let’s name the image views that are already there.
Click on the top image view, and in the Identity Inspector, set the name of the image view to “Basket Top”. Note this doesn’t affect anything in code, it just makes it easier to identify your views in the XIB. Repeat with the bottom image view, setting the name to Basket Bottom. At this point, your XIB should look like the following:
Using names in a XIB
Now, select the two Image Views in Interface Builder and go to Edit\Duplicate. Rename the two new views to “Napkin Top” and “Napkin Bottom”, and drag them under the UIView so they are children. Move the views above the two Basket views, since views are listed from bottom->top and you want the napkin to be below the basket. Finally, select each napkin image view individually, move it to the right spot, and set the image to XXX. At this point, your XIB should look like the following:
Adding napkins UIImageViewControllers to the XIB
Now that you have the new image views in your XIB, see if you can animate this yourself based on everything you’ve learned! The goal is to make the napkin move off screen also, but start moving slightly after the picnic basket starts moving. Go ahead – you can always check back here if you get stuck.

…waiting…

…waiitng…

…waiting…

Tomato-San is angry!
Tomato-San is angry!
What?! Are you still reading here?! You can do it – go ahead and try! :]

The Solution

In case you had any troubles, here’s the solution.
First add two new properties to PicnicViewController.h:
@property (assign) IBOutlet UIImageView *napkinTop;
@property (assign) IBOutlet UIImageView *napkinBottom;
Save your header, then in Interface Builder, control-click “File’s Owner” to bring up the menu of outlets, and connect the two napkin outlets to their appropriate image views.
Switch to PicnicViewController.m and make the following modifications:
// At top, under @implementation
@synthesize napkinTop;
@synthesize napkinBottom;
 
// At bottom of viewDidLoad
CGRect napkinTopFrame = napkinTop.frame;
napkinTopFrame.origin.y = -napkinTopFrame.size.height;    
CGRect napkinBottomFrame = napkinBottom.frame;
napkinBottomFrame.origin.y = self.view.bounds.size.height;
 
[UIView animateWithDuration:0.7
                      delay:1.2
                    options: UIViewAnimationCurveEaseOut
                 animations:^{
                     napkinTop.frame = napkinTopFrame;
                     napkinBottom.frame = napkinBottomFrame;
                 } 
                 completion:^(BOOL finished){
                     NSLog(@"Done!");
                 }];
Compile and run your code, and you should see the basket open in an even cooler manner!
Double door effect with UIView animation

How To Chain Animations

So far you’ve just been animating a single property on these UIViews – the frame. Also, you’ve done just a single animation, and then you were done.
However as mentioned earlier in this article, there are several other properties you can animate as well, and you can also trigger more animations to run after one animation completes. So let’s try this out by experimenting with animating two more properties (center and transform) and using some animation chaining!
But first – let’s add the inside of the picnic basket! Open up PicnicViewController.xib and drag yet another UIImageView as a subview of the View. Make sure it’s at the top of the list (so it’s underneath everything else) and set the image to plate_cheese.png. Resize the image view so it fills up the whole screen. You also might want to set the name of the Image View to “Contents” so it’s easy to identify.
There’s one more thing you have to add. Somehow, despite all of your precautions, a sneaky bug has made its way into the basket! Add another UIImageView as a subview of the View. Put it right underneath the Contents View, and set the image to bug.png. Set its frame to X 160, Y 185, width 135, height 142 in the Size Inspector. You also might want to set the name of the Image View to “Bug” so it’s easy to identify.
At this point, your XIB should look like the following:
Final XIB Layout
Next add a property for the new pest image view in PicnicViewController.h:
@property (assign) IBOutlet UIImageView *bug;
Save your header, switch back to Interface Builder, and connect the bug image view to the outlet in the same way you did with the other outlets earlier in this tutorial.
Next switch to PicnicViewController.m, and make the following modifications:
// At top, under @implementation
@synthesize bug;
 
// Add two new methods
- (void)moveToLeft:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
 
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:1.0];
    [UIView setAnimationDelay:2.0];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector:@selector(faceRight:finished:context:)];
    bug.center = CGPointMake(75, 200);
    [UIView commitAnimations];
 
}
 
- (void)faceRight:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
 
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:1.0];
    [UIView setAnimationDelay:0.0];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationDidStopSelector:@selector(moveToRight:finished:context:)];
    bug.transform = CGAffineTransformMakeRotation(M_PI);
    [UIView commitAnimations];
 
}
 
- (void)moveToRight:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
 
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:1.0];
    [UIView setAnimationDelay:2.0];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationDidStopSelector:@selector(faceLeft:finished:context:)];
    bug.center = CGPointMake(230, 250);
    [UIView commitAnimations];
 
}
 
- (void)faceLeft:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
 
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:1.0];
    [UIView setAnimationDelay:0.0];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
    [UIView setAnimationDidStopSelector:@selector(moveToLeft:finished:context:)];
    bug.transform = CGAffineTransformMakeRotation(0);
    [UIView commitAnimations];
 
}
 
// Call the method at the bottom of viewDidLoad
[self moveToLeft:nil finished:nil context:nil];
First, note that we’re using the pre-iOS4 method of running the animations here, instead of the iOS4+ method we’ve been using lately. This is because there is a problem you use the iOS4+ method – the view doesn’t respond to touches while the animations are running! I’m not sure if this is a bug or I’m doing something wrong, but either way we need touches later on in this tutorial so we’ll revert to the pre-iOS4 method.
Update: Deian from the comments section has pointed out that you can enable UIView to continue to respond to touches in the iOS4 method by using UIViewAnimationOptionAllowUserInteraction in the options, w00t!
Second, you can see that the way animation chaining is done is by a) using setAnimationDelegate to set the view controller as the delegate, and b) using setAnimationDidStopSelector to set up a callback when the animation finishes. The callback just starts up the next animation, and continues the chain. The chain is move left -> face right -> move right -> face left -> move left again, and continue.
Third, note that we’re moving the bug by using the center property rather than the frame property. This sets where the center of the bug image is, which is a little easier to do than modifying the frame sometimes.
Fourth, note that you can get the right transform to rotate the bug, by using a little helper function called CGAffineTransformMakeRotation. The angle here is in radians, so you use M_PI to rotate 180 degrees.
Compile and run the code, and you should see the bug scurrying about!
Eek!  A bug's in our picnic!

Squash the Bug!

Now it’s the moment I know you’ve been waiting for – it’s time to squash that bug!
But first we have to detect when you’re tapping the bug. Try adding this code to PicnicViewController.m first:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
 
    UITouch *touch = [touches anyObject];
    CGPoint touchLocation = [touch locationInView:self.view];
 
    CGRect bugRect = [bug frame];
    if (CGRectContainsPoint(bugRect, touchLocation)) {
        NSLog(@"Bug tapped!");
    } else {
        NSLog(@"Bug not tapped.");
        return;
    }
 
}
This implements the touch handler, and tries to compare the point tapped to the frame of the bug. Seems like that should work, right? Well if you compile and run, you’ll see something weird: it will appear to work sometimes, but after the bug rotates, if you tapped on the bug it won’t work. However if you click in the area he’s facing, it will say you tapped the bug (which you obviously didn’t) – WTF!
Well, the reason this doesn’t work is because when an animation is running, it only updates the position of the view in something called the “presentation layer”, which is what is shown to screen. We’ll talk more about layers and what this all means in a future tutorial, but for now all you have to do is replace the line that gets the bugRect with this:
CGRect bugRect = [[[bug layer] presentationLayer] frame];
Now you will get the correct frame of the bug even when it’s moving! Mwuahaha… you know what’s coming next! First add the following instance variable to PicnicViewController.h:
bool bugDead;
Then make the following modifications to PicnicViewController.m:
// At the beginning of faceLeft, moveToRight, faceRight, and moveToLeft:
if (bugDead) return; 
 
// At bottom of touchesBegan
bugDead = true;
[UIView animateWithDuration:0.7 
                      delay:0.0 
                    options:UIViewAnimationCurveEaseOut
                 animations:^{                              
                     bug.transform = CGAffineTransformMakeScale(1.25, 0.75);
                 } 
                 completion:^(BOOL finished) {  
                     [UIView animateWithDuration:2.0 
                                           delay:2.0 
                                         options:0
                                      animations:^{                      
                                          bug.alpha = 0.0;
                                      } completion:^(BOOL finished) {
                                          [bug removeFromSuperview];
                                          bug = nil;
                                      }];                 
                 }];
Once the bug is tapped, it sets bugDead to true (so the animation chain stops running), and then uses the iOS4+ method (since we don’t care about touches anymore and this is more terse) so set up a mini-chain of animations.
It first squishes the bug (by applying a scale transform), and then makes the bug fade away (by setting the alpha to 0 after a delay). Finally, when it’s done it removes the bug from its super view and sets it to nil.
Compile and run the code, and now you should be able to squash the bug!
One squashed bug!

Gratuitous Sound Effect

This is totally unnecessary, but also totally fun – let’s add a sound effect when we squash the bug!
In XCode, right click on the Frameworks group, and choose Add\Existing Frameworks…. Choose AudioToolbox.framework, and click Add.
Then mad the following modifications to PicnicViewController.m:
// At top of file
#import <AudioToolbox/AudioToolbox.h>
 
// At bottom of touchesBegan
NSString *squishPath = [[NSBundle mainBundle] 
                        pathForResource:@"squish" ofType:@"caf"];
NSURL *squishURL = [NSURL fileURLWithPath:squishPath];
SystemSoundID squishSoundID;
AudioServicesCreateSystemSoundID((CFURLRef)squishURL, &squishSoundID);
AudioServicesPlaySystemSound(squishSoundID);
And that’s it! Compile and run your code and you can now squash the bug and get major audio satisfaction in the process.

Where To Go From Here?

Here is a sample project with all of the code we’ve developed in the above tutorial.
Now that you know the basics of using UIView animations, you might want to take a look at the Animations section in the View Programming Guide for iOS for additional useful info.
Now that you know a bit about UIView animations, you might want to check out the next tutorial about one of the underlying technologies of animation – layers! There are a lot of cool things you can do with layers, but they can be confusing at first, so I wanted to talk about that. This tutorial is also a good segway into Core Animation, which is the technology UIView animation is based upon.
How do you use UIView animation or Core Animation in your projects?

No comments:

Post a Comment