Making a UIButton Flip Over
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.
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.