1

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:

  • the text of the UIButton sub-view is not shown after flipping
  • the shadow disappears during the flip animation

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
salocinx
  • 3,715
  • 8
  • 61
  • 110

3 Answers3

1

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
salocinx
  • 3,715
  • 8
  • 61
  • 110
0

Try to use:

    [UIView transitionFromView:self.subviews[0] toView:self.subviews[1] duration:0.5 UIViewAnimationOptionTransitionFlipFromLeft | UIViewAnimationOptionShowHideTransitionViews completion:^(BOOL finished) {
    }];
Vitalii Gozhenko
  • 9,220
  • 2
  • 48
  • 66
  • Thanks for your response. But your code unfortunately works even worse. The image changes before starting the animation with your code. The shadow is also not animated and the UIButton text is not shown. – salocinx Aug 20 '15 at 19:31
  • hum, try to `styleViewWithRoundedEdges` not your subviews, but the parent view `CardView`. I think in this case you don't have problems with shadow – Vitalii Gozhenko Aug 21 '15 at 12:25
  • Unfortunately no change in the result ... Any further ideas? – salocinx Aug 24 '15 at 16:42
0

This should work

[UIView transitionFromView:self.subviews[0] toView:self.subviews[1] duration:0.5 UIViewAnimationOptionTransitionFlipFromLeft | UIViewAnimationOptionShowHideTransitionViews completion:^(BOOL finished) {
}];
hakamairi
  • 4,464
  • 4
  • 30
  • 53