Combining Blocks, Properties and Animation in Objective-C

Blocks are a recent addition to iOS and Mac development. It’s a C-level feature, and is available in the XCode tools in Snow Leopard and for iOS 4.0 and later. The support in UIKit and Foundation libraries is still a work in progress, but a lot of important Cocoa classes already offer support for this language feature.

I’m not going to explain all the details of blocks. The Apple documentation does a fair job of that already. Good resources are Block Programming Topics and A Short Practical Guide To Blocks. Instead of rehashing that stuff, I’d like to explore some ideas of how to use blocks in our Objective-C programs. Learning any new language feature is like getting a new hammer for Christmas: You start looking for nails. I’m still early in my search for nails to hit with this hammer, but the first thing I wanted to try was inspired by my very limited knowledge of the javascript language.

Javascript, and other languages, have the concept of closures, which is a powerful but often poorly understood concept, but it forms the basis for a lot of the really cool stuff that’s recently emerged in web development, like jQuery. Blocks are essentially closures for Objective-C. And I wondered if you could therefore start assigning anonymous bits of code (and the variables in their scope) to object properties in Objective-C. It turns out you can, and it’s awesome!

I will step through some code examples to show you what I mean. And I’ve posted the sample project that all this code comes from here: https://github.com/silromen/ObjectiveScript

This sample project, called ObjectiveScript, is pretty simple. There’s an image we can animate by tapping it. We can switch how the image animates by tapping one of three buttons: Rotate, Scale and Transform. Once a button is tapped, we tap the image and it performs the chosen type of animation.

There are two main classes: the ViewController and the View which contains the image we want to animate.

Let’s start with the animateable view, here called SpecialView:


//  SpecialView.h

#import
@interface SpecialView : UIView {
	void (^animation)(void);
}

@property (nonatomic, copy) void (^animation)(void);
@end

Look at that property declaration! It’s saying that this object has a property called animation that we can assign an arbitrary block of code to, which returns nothing and takes no arguments. This is different than declaring a method, because the code in a method has to be defined at compile time. With a block, we can assign any code we want to that property at run time.

Now let’s look at the source code for our SpecialView.


//  SpecialView.m

#import "SpecialView.h"

@implementation SpecialView
@synthesize animation;

- (void)dealloc {
    [super dealloc];
	[animation release];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
	[UIView animateWithDuration:0.85
						  delay:0
						options:UIViewAnimationOptionRepeat|UIViewAnimationOptionAutoreverse
					 animations:self.animation
					 completion:^(BOOL finished){
						 self.transform = CGAffineTransformIdentity;
					 }];
}
@end

There’s not a lot of code, but there’s a lot going on here. We’re synthesizing and releasing our property – that’s normal. We’re overriding touchesEnded and defining an animation we want to occur whenever a touch event ends on this view. Now, in the old days (last year), without going deep into Core Animation Layers, you animated a view by defining some changes to the view you wanted animated and bookending those instructions with [UIView beginAnimations: context:] and [UIView commitAnimations], and if it pleased you, you could specify a selector to run when the animation completed. But since iOS 4.0, there’s a block-friendly way of doing exactly the same thing, and that’s what is happening in this code sample. We’re calling the class method animateWithDuration:delay:options:animations:completion:. The animations and completion parameters are accepting blocks of anonymous code. What’s really special in this example, is we’re passing the contents of our view’s animation property as the block to execute for the animation. The completion code is always going to be the same, which is to undo any transformation that the animation has performed on the view by setting the UIView transform property to the identity matrix. But the animation itself could be any chunk of code we assign that animation property to.

So let’s see how the ViewController puts that cool feature into use.


//  ObjectiveScriptViewController.h

#import
#import "SpecialView.h"

@interface ObjectiveScriptViewController : UIViewController {
	IBOutlet SpecialView *specialView;
	IBOutlet UIButton *rotateButton;
	IBOutlet UIButton *translateButton;
	IBOutlet UIButton *scaleButton;
}

- (IBAction)onRotate:(id)sender;
- (IBAction)onTranslate:(id)sender;
- (IBAction)onScale:(id)sender;
- (void)setSelected:(UIButton*)sender;
@end

In the header, we have our outlets for the special view and the buttons. We have our handlers which are called in response to the buttons being touched, and we have a helper method that will be used to make the three buttons behave like radio buttons, so only one is selected at a time.

 
//  ObjectiveScriptViewController.m

#import "ObjectiveScriptViewController.h"

@implementation ObjectiveScriptViewController

- (void)dealloc {
	[specialView release];
        [super dealloc];
}

- (IBAction)onRotate:(id)sender {
	specialView.animation = ^{
		 [UIView setAnimationRepeatCount:1.0];
		specialView.transform = CGAffineTransformRotate(CGAffineTransformIdentity, 135.0);
	};
	[self setSelected:(UIButton*)sender];
}

- (IBAction)onTranslate:(id)sender {
	specialView.animation = ^{
		 [UIView setAnimationRepeatCount:1.0];
		specialView.transform = CGAffineTransformMakeTranslation(-175, -175);
	};
	[self setSelected:(UIButton*)sender];
}

- (IBAction)onScale:(id)sender {
	specialView.animation = ^{
		 [UIView setAnimationRepeatCount:1.0];
		specialView.transform = CGAffineTransformMakeScale(0.1, 0.1);
	};
	[self setSelected:(UIButton*)sender];
}

- (void)setSelected:(UIButton*)sender {
	rotateButton.selected = ([sender isEqual:rotateButton]) ? YES : NO;
	translateButton.selected = ([sender isEqual:translateButton]) ? YES : NO;
	scaleButton.selected = ([sender isEqual:scaleButton]) ? YES : NO;
}
@end

Notice how each button’s handler method assigns a chunk of animation code to the SpecialView’s animation property. Tapping these buttons doesn’t execute the animation, but they tell the view which animations to perform by feeding it the code to execute.

The combination of blocks, properties and core animation is powerful, and this example is just scratching the surface. This gives developers another way to conceptualize and organize their code, and having more options is always a good thing.

Advertisements

Mixing PopoverControllers with ModalViewControllers

When building an app on the iPad, sometimes you need to display a Popover, and tapping a button on that Popover will launch a Modal View. When you’re done with the Modal View, it flies away (gotta love those animations), and you’re back to the Popover.

This doesn’t seem like a stretch, but if you’re not careful in how you do it, iOS has some nasty surprises in store for you.

So here’s what you’re trying to accomplish:

Let’s take a first attempt at the code for this:

@implementation ModalPlusPopoverViewController
...
// When the Go Button is pressed, we create a view controller, embed it in a popover controller and present it.
- (IBAction)goPressed:(id)sender {
	MyViewController *theViewController = [[[MyViewController alloc] initWithNibName:@"MyViewController" bundle:nil] autorelease];
	UIPopoverController *popoverController = [[UIPopoverController alloc] initWithContentViewController:theViewController];
	[popoverController setPopoverContentSize:CGSizeMake(400, 400) animated:NO];
	[popoverController presentPopoverFromBarButtonItem:(UIBarButtonItem *)sender
						     permittedArrowDirections:UIPopoverArrowDirectionUp
									       animated:YES];

}
@end
@implementation MyViewController
...
// Inside the popover, when the "Launch Modal" button is pressed, we create our modal view controller and present it
- (IBAction)launchModal:(id)sender {
	MyModalViewController *theModalViewController = [[[MyModalViewController alloc] initWithNibName:@"MyModalViewController" bundle:nil] autorelease];
	theModalViewController.modalPresentationStyle = UIModalPresentationFormSheet;
	[self presentModalViewController:theModalViewController animated:YES];
}
@end
@implementation MyModalViewController
...
// To dismiss the modal, the user taps the Dismiss button, and we dismiss ourselves
- (IBAction)onDismiss:(id)sender {
	[self.parentViewController dismissModalViewControllerAnimated:YES];
}
@end

With this code, everything seems to work well at first glance. When you tap Launch Modal, the modal view slides up from the bottom, and when you tap Dismiss, it slides down again.

But now rotate the iPad and try again. This time, the Modal view slides up from the bottom, but when you dismiss it, it flies out to the left!

And it gets worse. If your modal view happens to be a MFMailComposeViewController, which is a handy class to make it easy for users to send email directly from your app, when you try to dismiss it, it actually causes your app to rotate, even though the iPad is still in landscape orientation.

So what’s going on here?

When you launch and dismiss a modal view from a popover, iOS is doing some strange things under the hood. If you put a breakpoint on shouldAutoRotateToInterfaceOrientation in your popover view controller, you’ll find it gets called when you launch and dismiss the modal. If you examine the call stack you’ll see this call:
[UIViewController _preferredInterfaceOrientationGivenCurrentOrientation:]
So there’s some calculation going on under the hood that’s making an orientation decision. When your modal view is MFMailComposeViewController and you dismiss it, you actually get the wrong orientation passed to shouldAutoRotateToInterfaceOrientation: you get UIInterfaceOrientationPortrait when what you’re expecting is UIInterfaceOrientationLandscapeRight. And this is why your iPad display rotates. And I suspect some similar behaviour is causing the modal view to slide out in the wrong direction.

So how do we fix this?

It turns out our mistake is launching the modal view from the popover. It’s this call from the MyViewController class:
[self presentModalViewController:theModalViewController animated:YES];
Here, self refers to the MyViewController class, which is embedded in a Popover. What we need to do is launch the modal view from the underlying view controller, the one that launched the popover view. In our example, that’s ModalPlusPopoverViewController. The problem is the view controller in the popover has no reference to the underlying controller, so we need to add code for that. Our fixed code looks like this:

@implementation ModalPlusPopoverViewController
...
- (IBAction)goPressed:(id)sender {
	MyViewController *theViewController = [[[MyViewController alloc] initWithNibName:@"MyViewController" bundle:nil] autorelease];
       // We now tell the view controller we're embedding in the popover that it should launch modal views from this view controller
	theViewController.baseViewController = self;
	UIPopoverController *popoverController = [[UIPopoverController alloc] initWithContentViewController:theViewController];
	[popoverController setPopoverContentSize:CGSizeMake(400, 400) animated:NO];
	[popoverController presentPopoverFromBarButtonItem:(UIBarButtonItem *)sender
							  permittedArrowDirections:UIPopoverArrowDirectionUp
											  animated:YES];

}
@end
@implementation MyViewController
@synthesize baseViewController;
...
- (IBAction)launchModal:(id)sender {
	MyModalViewController *theModalViewController = [[[MyModalViewController alloc] initWithNibName:@"MyModalViewController" bundle:nil] autorelease];
	theModalViewController.modalPresentationStyle = UIModalPresentationFormSheet;
	[baseViewController presentModalViewController:theModalViewController animated:YES];
}
@end

I’ve omitted the header file code here, but it should be clear what the header files contain based on the source. Now our modal view behaves as expected when we dismiss it. And if we’re presenting a MFMailComposeViewController, it doesn’t auto-rotate the app behind our backs.

The moral of this story is to be careful which objects you launch your modal views from, especially when you’re launching them from Popovers.