Question

This question has been asked before but in a slightly different way and I was unable to get any of the answers to work the way I wanted, so I am hoping somebody with great Core Animation skills can help me out.

I have a set of cards on a table. As the user swipes up or down the set of cards move up and down the table. There are 4 cards visible on the screen at any given time, but only the second card is showing its face. As the user swipes the second card flips back onto its face and the next card (depending on the swipe direction) lands in it's place showing its face.

I have set up my card view class like this:

@interface WLCard : UIView {
    UIView *_frontView;
    UIView *_backView;
    BOOL flipped;
}

And I have tried flipping the card using this piece of code:

- (void) flipCard {
    [self.flipTimer invalidate];
    if (flipped){
        return;
    }

    id animationsBlock = ^{
            self.backView.alpha = 1.0f;
            self.frontView.alpha = 0.0f;
            [self bringSubviewToFront:self.frontView];
            flipped = YES;

            CALayer *layer = self.layer;
            CATransform3D rotationAndPerspectiveTransform = CATransform3DIdentity;
            rotationAndPerspectiveTransform.m34 = 1.0 / 500;
            rotationAndPerspectiveTransform = CATransform3DRotate(rotationAndPerspectiveTransform, M_PI, 1.0f, 0.0f, 0.0f);
            layer.transform = rotationAndPerspectiveTransform;
    };
    [UIView animateWithDuration:0.25
                          delay:0.0
                        options: UIViewAnimationCurveEaseInOut
                     animations:animationsBlock
                     completion:nil];

}

This code works but it has the following problems with it that I can't seem to figure out:

  1. Only half of the card across the x-axis is animated.
  2. Once flipped, the face of the card is upside down and mirrored.
  3. Once I've flipped the card I cannot get the animation to ever run again. In other words, I can run the animation block as many times as I want, but only the first time will animate. The subsequent times I try to animate lead to just a fade in and out between the subviews.

Also, bear in mind that I need to be able to interact with the face of the card. i.e. it has buttons on it.

If anybody has run into these issues it would be great to see your solutions. Even better would be to add a perspective transform to the animation to give it that extra bit of realism.

Was it helpful?

Solution

This turned out to be way simpler than I thought and I didn't have to use any CoreAnimation libraries to achieve the effect. Thanks to @Aaron Hayman for the clue. I used transitionWithView:duration:options:animations:completion

My implementation inside the container view:

    [UIView transitionWithView:self 
                      duration:0.2 
                       options:UIViewAnimationOptionTransitionFlipFromBottom
                    animations: ^{
                            [self.backView removeFromSuperview];
                            [self addSubview:self.frontView];
                    }
                    completion:NULL];

The trick was the UIViewAnimationOptionTransitionFlipFromBottom option. Incidentally, Apple has this exact bit of code in their documentation. You can also add other animations to the block like resizing and moving.

OTHER TIPS

Ok, this won't be a complete solution but I'll point out some things that might be helpful. I'm not a Core-Animation guru but I have done a few 3D rotations in my program.

First, there is no 'back' to a view. So if you rotate something by M_PI (180 degrees) you're going to be looking at that view as though from the back (which is why it's upside down/mirrored).

I'm not sure what you mean by:

Only half of the card across the x-axis is animated.

But, it it might help to consider your anchor point (the point at which the rotation occurs). It's usually in the center, but often you need it to be otherwise. Note that anchor points are expressed as a proportion (percentage / 100)...so the values are 0 - 1.0f. You only need to set it once (unless you need it to change). Here's how you access the anchor point:

layer.anchorPoint = CGPointMake(0.5f, 0.5f) //This is center

The reason the animation only ever runs once is because transforms are absolute, not cumulative. Consider that you're always starting with the identity transform and then modifying that, and it'll make sense...but basically, no animation occurs because there's nothing to animate the second time (the view is already in the state you're requesting it to be in).

If you're animating from one view to another (and you can't use [UIView transitionWithView:duration:options:animations:completion:];) you'l have to use a two-stage animation. In the first stage of the animation, for the 'card' that is being flipped to backside, you'll rotate the view-to-disappear 'up/down/whatever' to M_PI_2 (at which point it will be 'gone', or not visible, because of it's rotation). And in the second stage, you're rotate the backside-of-view-to-disappear to 0 (which should be the identity transform...aka, the view's normal state). In addition, you'll have to do the exact opposite for the 'card' that is appearing (to frontside). You can do this by implementing another [UIView animateWithDuration:...] in the completion block of the first one. I'll warn you though, doing this can get a little bit complicated. Especially since you're wanting views to have a 'backside', which will basically require animating 4 views (the view-to-disappear, the view-to-appear, backside-of-view-to-disappear, and the backside-of-view-to-appear). Finally, in the completion block of the second animation you can do some cleanup (reset view that are rotated and make their alpha 0.0f, etc...).

I know this is complicated, so you might want read some tutorial on Core-Animation.

@Aaron has some good info that you should read.

The simplest solution is to use a CATransformLayer that will allow you to place other CALayer's inside and maintain their 3D hierarchy.

For example to create a "Card" that has a front and back you could do something like this:

CATransformLayer *cardContainer = [CATransformLayer layer];
cardContainer.frame = // some frame;

CALayer *cardFront  = [CALayer layer];
cardFront.frame     = cardContainer.bounds;
cardFront.zPosition = 2;   // Higher than the zPosition of the back of the card
cardFront.contents  = (id)[UIImage imageNamed:@"cardFront"].CGImage;
[cardContainer addSublayer:cardFront];

CALayer *cardBack  = [CALayer layer];
cardBack.frame     = cardContainer.bounds;
cardBack.zPosition = 1;
cardBack.contents  = (id)[UIImage imageNamed:@"cardBack"].CGImage; // You may need to mirror this image
[cardContainer addSublayer:cardBack];

With this you can now apply your transform to cardContainer and have a flipping card.

@Paul.s

I followed your approach with card container but when i applt the rotation animation on card container only one half of the first card rotates around itself and finally the whole view appears.Each time one side is missing in the animation

Based on Paul.s this is updated for Swift 3 and will flip a card diagonally:

func createLayers(){
  transformationLayer = CATransformLayer(layer: CALayer())
  transformationLayer.frame = CGRect(x: 15, y: 100, width: view.frame.width - 30, height: view.frame.width - 30)

  let black = CALayer()
  black.zPosition = 2
  black.frame = transformationLayer.bounds
  black.backgroundColor = UIColor.black.cgColor

  transformationLayer.addSublayer(black)

  let blue = CALayer()
  blue.frame = transformationLayer.bounds
  blue.zPosition = 1
  blue.backgroundColor = UIColor.blue.cgColor

  transformationLayer.addSublayer(blue)

  let tgr = UITapGestureRecognizer(target: self, action: #selector(recTap))
  view.addGestureRecognizer(tgr)
  view.layer.addSublayer(transformationLayer)
}

Animate a full 360 but since the layers have different zPositions the different 'sides' of the layers will show

func recTap(){
    let animation = CABasicAnimation(keyPath: "transform")
    animation.delegate = self
    animation.duration = 2.0
    animation.fillMode = kCAFillModeForwards
    animation.isRemovedOnCompletion = false
    animation.toValue = NSValue(caTransform3D: CATransform3DMakeRotation(CGFloat(Float.pi), 1, -1, 0))
    transformationLayer.add(animation, forKey: "arbitrarykey")
  }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top