If you’ve used the iPod app on the iPhone, you’ve probably seen an interesting trick when switching from album art to the track listing: the button in the top right corner flips over synchronized with the flip animation of the main view.

I wanted to achieve a similar effect for the iPhone app that I’m building, Deli Radio (app store link).

If you’ve ever struggled with the flip transitions before, you probably know how finicky it can be to get it all working. The best way to set it up is to have a parent view contain both views that you want to swap, and set the animation transition type on that view.

For a button (either in the navigation bar or elsewhere) we’d need to introduce a parent view to achieve this effect. This is how I achieved the effect.

btn-info-border@2x btn-images@2x

First, I had two images I wanted to use for my UIBarButtonItem.

I wanted this to be easily reusable (since I need to do this in more than one place in this application), so I created a category method on UIButton.

@interface UIButton (CHFlipButton)

+ (UIView *)flipButtonWithFirstImage:(UIImage *)firstImage 
                         secondImage:(UIImage *)secondImage
                     firstTransition:(UIViewAnimationTransition)firstTransition
                    secondTransition:(UIViewAnimationTransition)secondTransition
                      animationCurve:(UIViewAnimationCurve)curve
                            duration:(NSTimeInterval)duration
                              target:(id)target
                            selector:(SEL)selector;


@end

It may look strange that a UIButton class method returns a UIView instance, but we need to have a container view to base the animations off of.

Here is the implementation:

#import <objc/runtime.h>

#import "UIButton+CHFlipButton.h"

// keys used for assigning associated objects
static char UIButtonFlipBlockKey;
static char UIButtonFlipAltButtonKey;
static char UIButtonFlipTransitionKey;
static char UIButtonFlipContainerViewKey;

typedef void (^UIButtonFlipActionBlock)(id sender);

@implementation UIButton (CHFlipButton)

// associate the block with the button instance, then set the default button handler

- (void)chFlipButton_handleControlEvent:(UIControlEvents)event withBlock:(UIButtonFlipActionBlock)block {
    objc_setAssociatedObject(self, &UIButtonFlipBlockKey, block, OBJC_ASSOCIATION_COPY);
    [self addTarget:self action:@selector(chFlipButton_callFlipBlock:) forControlEvents:UIControlEventTouchUpInside];
}

// the default button handler just calls the block

- (void)chFlipButton_callFlipBlock:(id)sender {
    UIButtonFlipActionBlock block = objc_getAssociatedObject(self, &UIButtonFlipBlockKey);
    if (block) {
        block(sender);
    }
}

+ (UIView *)flipButtonWithFirstImage:(UIImage *)firstImage 
                         secondImage:(UIImage *)secondImage
                     firstTransition:(UIViewAnimationTransition)firstTransition
                    secondTransition:(UIViewAnimationTransition)secondTransition
                      animationCurve:(UIViewAnimationCurve)curve
                            duration:(NSTimeInterval)duration
                              target:(id)target
                            selector:(SEL)selector {
    
    UIButtonFlipActionBlock flipButtonAction = ^(id sender) { /* shown further down */ };
    
    //create the first button
    UIButton *button1 = [UIButton buttonWithType:UIButtonTypeCustom];
    [button1 setBackgroundImage:firstImage forState:UIControlStateNormal];
    [button1 chFlipButton_handleControlEvent:UIControlEventTouchUpInside withBlock:flipButtonAction];
    [button1 setFrame:CGRectMake(0, 0, firstImage.size.width, firstImage.size.height)];

    //create the 2nd button
    UIButton *button2 = [UIButton buttonWithType:UIButtonTypeCustom];
    [button2 setBackgroundImage:secondImage forState:UIControlStateNormal];
    [button2 chFlipButton_handleControlEvent:UIControlEventTouchUpInside withBlock:flipButtonAction];
    [button2 setFrame:CGRectMake(0, 0, secondImage.size.width, secondImage.size.height)];    
 
    //create a container to hold them
    UIView *container = [[[UIView alloc] initWithFrame:button1.bounds] autorelease];
    [container addSubview:button1];
    
    //record state so we can access it later (in the block)
    objc_setAssociatedObject(button1, &UIButtonFlipAltButtonKey, button2, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(button1, &UIButtonFlipTransitionKey, [NSNumber numberWithInt:firstTransition], OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    objc_setAssociatedObject(button1, &UIButtonFlipContainerViewKey, container, OBJC_ASSOCIATION_ASSIGN);
    
    objc_setAssociatedObject(button2, &UIButtonFlipAltButtonKey, button1, OBJC_ASSOCIATION_ASSIGN);
    objc_setAssociatedObject(button2, &UIButtonFlipTransitionKey, [NSNumber numberWithInt:secondTransition], OBJC_ASSOCIATION_ASSIGN);  //button1 is in charge of the retains initially
    objc_setAssociatedObject(button2, &UIButtonFlipContainerViewKey, container, OBJC_ASSOCIATION_ASSIGN);
    
    //returns the container, because this is what needs to be added to your view
    return container;
}

@end

I am using a little-known technique of setting associated objects using objc_setAssociatedObject(...). This uses the runtime to attach state to an existing class without needing to subclass.

Now that you understand how it is all setup, the block body will now make sense:

/* Here is that block definition from above */

    UIButtonFlipActionBlock flipButtonAction = ^(id sender) {

        //get the alternate button & container
        UIButton *otherButton = (UIButton *)objc_getAssociatedObject(sender, &UIButtonFlipAltButtonKey);
        UIView *container = (UIView *)objc_getAssociatedObject(sender, &UIButtonFlipContainerViewKey);
        
        //figure out our transition
        NSNumber *transitionNumber = (NSNumber *)objc_getAssociatedObject(sender, &UIButtonFlipTransitionKey);
        UIViewAnimationTransition transition = (UIViewAnimationTransition)[transitionNumber intValue];
                
        [UIView animateWithDuration:duration animations:^ {
            
            [UIView setAnimationTransition:transition forView:container cache:YES];
            [UIView setAnimationCurve:curve];
            
            //the view has the last retain count on the sender button, so we need to retain it first
            objc_setAssociatedObject(otherButton, &UIButtonFlipAltButtonKey, sender, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            
            [sender removeFromSuperview];
            [container addSubview:otherButton];
            
            //sender no longer needs to retain the other button, because the view now is...
            objc_setAssociatedObject(sender, &UIButtonFlipAltButtonKey, otherButton, OBJC_ASSOCIATION_ASSIGN);
            
        }];
        
        //call the original button handler
        [target performSelector:selector withObject:self];
    };

Usage is really easy. I just created a bar button item with a custom view, and was done.

    UIImage *firstImage  = [UIImage imageNamed:@"btn-info.png"];
    UIImage *secondImage = [UIImage imageNamed:@"btn-images.png"];
    UIView *container    = [UIButton flipButtonWithFirstImage:firstImage
                                                  secondImage:secondImage
                                              firstTransition:UIViewAnimationTransitionFlipFromRight
                                             secondTransition:UIViewAnimationTransitionFlipFromLeft
                                               animationCurve:UIViewAnimationCurveEaseInOut
                                                     duration:0.8
                                                       target:self
                                                     selector:@selector(flipContent)];

    self.navigationItem.rightBarButtonItem = [[[UIBarButtonItem alloc] 
      initWithCustomView:container] autorelease];

The effect can be seen below.

Note that the flip effect on the main view is achieved separately, but the 2 strategies share identical values for the animation, so the flip transition types match, as well as the duration & animation curve.