iosobjective-cuikitcore-animationuikit-transitions

iOS: How to animate a flip between two UIViews?


I am trying to animate a flip effect between a UIButton instance and a UIImageView instance. Basically it's a "flip over a playing card" effect, one side (UIImageView) is just a nice pattern and when flipped, it should show a UIButton with some text.

My code features the following problems:

Here's a visual representation of the goal:

enter image description here

Here you can download the very simple sample app.

Any suggestions how to solve the two mentioned issues ?

I am really out of ideas - any help highly appreciated!

Here's the header code:

#import <UIKit/UIKit.h>

@interface CardView : UIControl

@property (nonatomic) BOOL isFrontSide;

- (void)setupView;
- (void)turnCard:(BOOL)inShow withAnimationCompletion:(void (^)(BOOL inFinished))inCompletion;

@end

Here's the implementation code:

#import "CardView.h"
#import "UIView+Extension.h"
#import <QuartzCore/QuartzCore.h>

#define kAllControlStates (UIControlStateNormal | UIControlStateHighlighted | UIControlStateDisabled| UIControlStateSelected)

@interface CardView()

@end

@implementation CardView

- (void)setupView {

    [self styleViewWithRoundedEdges:YES shadowed:YES];

    UIImageView *theBackView = [[UIImageView alloc] initWithFrame:self.bounds];
    theBackView.image = [UIImage imageNamed:@"pattern.png"];
    theBackView.hidden = NO;
    theBackView.userInteractionEnabled = NO;
    [theBackView styleViewWithRoundedEdges:YES shadowed:NO];
    [self addSubview:theBackView];

    UIButton *theFrontView = [[UIButton alloc] initWithFrame:self.bounds];
    [theFrontView setTitle:@"Push me !" forState:kAllControlStates];
    theFrontView.hidden = YES;
    theFrontView.userInteractionEnabled = NO;
    [theFrontView styleViewWithRoundedEdges:YES shadowed:NO];
    [self addSubview:theFrontView];

}

- (void)turnCard:(BOOL)inShow withAnimationCompletion:(void (^)(BOOL inFinished))inCompletion { 
    [UIView transitionWithView:self duration:0.75 
                       options:inShow ? UIViewAnimationOptionTransitionFlipFromLeft : UIViewAnimationOptionTransitionFlipFromRight
                    animations:^{
                        [(self.subviews)[0] setHidden:inShow];      // UIImage
                        [(self.subviews)[1] setHidden:!inShow];     // UIButton
                    } 
                    completion:inCompletion];
}

@end

Here's a category to visually decorate my views:

#import "UIView+Extension.h"

@implementation UIView (Extension)

- (void)styleViewWithRoundedEdges:(BOOL)rounded shadowed:(BOOL)shadowed {
    [self styleViewWithRoundedEdges:rounded shadowed:shadowed rasterized:YES];
}

- (void)styleViewWithRoundedEdges:(BOOL)rounded shadowed:(BOOL)shadowed rasterized:(BOOL)rasterized {
    if (rounded) {
        self.layer.cornerRadius = 3.0;
    }
    if (shadowed) {
        self.layer.shadowColor = [UIColor blackColor].CGColor;
        self.layer.shadowOffset = CGSizeMake(2.0, 2.0);
        self.layer.shadowOpacity = 0.25;
        self.layer.shadowRadius = 1.0;
        if(rasterized) {
            self.layer.shouldRasterize = YES;
            self.layer.rasterizationScale = UIScreen.mainScreen.scale;
        }
    }
}

@end

Solution

  • Here's my working solution with shadow, rounded corners and no performance issue:

    Call [self.yourUICardButtonInstance setupWithImage:[UIImage imageNamed:@"your-image.png"]]; within the view controller's viewDidLayoutSubviews callback.

    Header:

    //
    //  UICardButton.h
    //  CardFlipDemo
    //
    //  Created by Nicolas Baumgardt on 25/08/15.
    //  Copyright (c) 2015 Nicolas Baumgardt. All rights reserved.
    //
    
    #import <UIKit/UIKit.h>
    
    @interface UICardButton : UIButton
    
    - (void)setupWithImage:(UIImage*)backside;
    
    - (void)flip;
    - (void)flipFrontside;
    - (void)flipBackside;
    
    @end
    

    Implementation:

    //
    //  UICardButton.m
    //  CardFlipDemo
    //
    //  Created by Nicolas Baumgardt on 25/08/15.
    //  Copyright (c) 2015 Nicolas Baumgardt. All rights reserved.
    //
    
    #import "UICardButton.h"
    
    @interface UICardButton ()
    
    @property (nonatomic) BOOL setup;
    
    @property (nonatomic, strong) NSString* text;
    @property (nonatomic, strong) UIImageView* subview;
    
    @property (nonatomic) BOOL IBInspectable styled;
    @property (nonatomic) BOOL IBInspectable frontside;
    
    @end
    
    @implementation UICardButton
    
    - (void)setupWithImage:(UIImage*)backside {
        if (!self.setup) {
            // cache properties
            self.text = self.currentTitle;
            self.subview = [[UIImageView alloc] initWithFrame:self.bounds];
            [self.subview setImage:backside];
            // initialize card side
            if (self.frontside) {
                // frontside: with text
                self.layer.transform = CATransform3DMakeRotation(0.0, 0.0, 1.0, 0.0);
                [self setAttributedTitle:nil forState:UIControlStateNormal];
                [self setTitle:self.text forState:UIControlStateNormal];
            } else {
                // backside: with image
                self.layer.transform = CATransform3DMakeRotation(M_PI, 0.0, 1.0, 0.0);
                [self addSubview:self.subview];
                [self setAttributedTitle:nil forState:UIControlStateNormal];
                [self setTitle:@"" forState:UIControlStateNormal];
            }
            // add a shadow by wrapping the button into a container and add rounded corners
            if (self.styled) {
                // add a shadow
                self.layer.shadowColor = [UIColor blackColor].CGColor;
                self.layer.shadowOffset = self.frontside ? CGSizeMake(2.0, 2.0) : CGSizeMake(-2.0, 2.0);
                self.layer.shadowOpacity = 0.25;
                self.layer.shadowRadius = 1.0;
                self.layer.cornerRadius = 3.0;
                self.layer.masksToBounds = NO;
                // clip card image
                self.subview.layer.masksToBounds = YES;
                self.subview.layer.cornerRadius = 3.0;
                // INFO: rasterization sometimes causes flickering, but enormous performance boost !
                self.layer.shouldRasterize = YES;
                self.layer.rasterizationScale = UIScreen.mainScreen.scale;
            }
            self.setup = YES;
        }
    }
    
    - (void)flip {
        if (self.frontside) {
            [self flipBackside];
        } else {
            [self flipFrontside];
        }
        self.frontside = !self.frontside;
    }
    
    - (void)flipFrontside {
        self.userInteractionEnabled = NO;
        [UIView animateWithDuration:0.25 delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
            self.layer.transform = CATransform3DMakeRotation(M_PI_2, 0.0, 1.0, 0.0);
        } completion:^(BOOL finished) {
            [self.subview removeFromSuperview];
            [self setAttributedTitle:nil forState:UIControlStateNormal];
            [self setTitle:self.text forState:UIControlStateNormal];
            self.layer.shadowOffset = CGSizeMake(2.0, 2.0);
            [UIView animateWithDuration:0.25 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
                self.layer.transform = CATransform3DMakeRotation(0.0, 0.0, 1.0, 0.0);
            } completion:^(BOOL finished) {
                self.userInteractionEnabled = YES;
            }];
        }];
    }
    
    - (void)flipBackside {
        self.userInteractionEnabled = NO;
        [UIView animateWithDuration:0.25 delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
            self.layer.transform = CATransform3DMakeRotation(M_PI_2, 0.0, 1.0, 0.0);
        } completion:^(BOOL finished) {
            [self addSubview:self.subview];
            [self setAttributedTitle:nil forState:UIControlStateNormal];
            [self setTitle:@"" forState:UIControlStateNormal];
            self.layer.shadowOffset = CGSizeMake(-2.0, 2.0);
            [UIView animateWithDuration:0.25 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
                self.layer.transform = CATransform3DMakeRotation(M_PI, 0.0, 1.0, 0.0);
            } completion:^(BOOL finished) {
                self.userInteractionEnabled = YES;
            }];
        }];
    }
    
    @end