jefflovejapan
jefflovejapan

Reputation: 2121

Core Animation interactive flip transition like playing card with front and back

I'm trying to recreate Apple's UIView transitionFromView:ToView: with Core Animation so I can use it with an interactive view controller transition. It's almost there, but I can't get the "to" view controller's view to show up on the back of the card.

CATransform3D inRotation = CATransform3DMakeRotation(M_PI, 0.0, 1.0, 0.0);
inRotation.m34 = -0.02;
CATransform3D outRotation = CATransform3DMakeRotation(-0.01, 0.0, 1.0, 0.0);
outRotation.m34 = -0.02;

[UIView animateKeyframesWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:0 animations:^{
    [UIView addKeyframeWithRelativeStartTime:0.0
                            relativeDuration:0.5
                                  animations:^{
                                      fromViewController.view.layer.transform = inRotation;
                                  }];
    [UIView addKeyframeWithRelativeStartTime:0.5
                            relativeDuration:0.5
                                  animations:^{
                                      toViewController.view.layer.transform = outRotation;
                                  }];
} completion:^(BOOL finished) {
    [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];

I think I need to swap the two views (fromViewController.view and toViewController.view) somehow halfway through the transition but I can't seem to find a solution anywhere.

Upvotes: 1

Views: 1099

Answers (3)

Cœur
Cœur

Reputation: 38667

Your inRotation should have been with M_PI_2 (or -M_PI_2) instead of M_PI. The following solution is closer to your original code and it avoids your hacky workaround of just over or just short of pi by performing two quarter rotations:

func flip(_ flipToFront: Bool) {
    let fromView: UIView = flipToFront ? toViewController.view! : fromViewController.view!
    let toView: UIView = flipToFront ? fromViewController.view! : toViewController.view!

    fromView.superview!.insertSubview(fromView, belowSubview: toView)

    var initialToRotation: CATransform3D = CATransform3DMakeRotation(.pi, 0, 1, 0)
    initialToRotation.m34 = -0.004
    toView.layer.transform = initialToRotation

    var initialFromRotation: CATransform3D = CATransform3DIdentity
    initialFromRotation.m34 = -0.004
    fromView.layer.transform = initialFromRotation

    UIView.animateKeyframes(withDuration: 0.5, delay: 0, options: .calculationModeLinear, animations: {
        UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5, animations: {
            fromView.layer.transform = CATransform3DRotate(initialFromRotation, .pi / (flipToFront ? 2 : -2), 0, 1, 0)
            toView.layer.transform = CATransform3DRotate(initialToRotation, .pi / (flipToFront ? -2 : 2), 0, 1, 0)
        })
        UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5, animations: {
            fromView.layer.transform = CATransform3DRotate(initialFromRotation, .pi, 0, 1, 0)
            toView.layer.transform = CATransform3DRotate(initialToRotation, .pi, 0, 1, 0)
        })
    }, completion: nil)
}

Where I previously set:

override func viewDidLoad() {
    super.viewDidLoad()

    fromViewController.view.layer.isDoubleSided = false
    toViewController.view.layer.isDoubleSided = false
}

Upvotes: 0

jefflovejapan
jefflovejapan

Reputation: 2121

I'm not sure if this is the most idiomatic way to do this but I have a working solution:

[fromViewController.view.layer setDoubleSided:NO];
[toViewController.view.layer setDoubleSided:NO];

[container insertSubview:toViewController.view belowSubview:fromViewController.view];

CGFloat h = 0.0001;

CATransform3D initialToRotation = CATransform3DMakeRotation(M_PI - h, 0.0, 1.0, 0.0);
initialToRotation.m34 = -0.002;
[toViewController.view.layer setTransform:initialToRotation];

CATransform3D initialFromRotation = CATransform3DIdentity;
initialFromRotation.m34 = -0.002;
[fromViewController.view.layer setTransform:initialFromRotation];

[UIView animateWithDuration:0.5 animations:^{
    [fromViewController.view.layer setTransform:CATransform3DRotate(initialFromRotation, M_PI - h, 0.0, 1.0, 0.0)];
    [toViewController.view.layer setTransform:CATransform3DRotate(initialToRotation, M_PI + h, 0.0, 1.0, 0.0)];
} completion:^(BOOL finished) {
    BOOL wasCanceled = [transitionContext transitionWasCancelled];
    if (wasCanceled) {
        [transitionContext completeTransition:NO];
    } else {
        [transitionContext completeTransition:YES];
    }
}];

The important thing is that both the front and back need to rotate together, and the front needs to be rotated in advance of the transition. You set up your view hierarchy in the container view but the transformation code is called on the viewControllers' views directly. Also, CoreAnimation doesn't seem to have any notion of clockwise or counter-clockwise, so you need to rotate either just over or just short of pi radians in order to ensure the right behavior.

Upvotes: 1

Bot
Bot

Reputation: 11845

I have a project that does this. Here is what we use for this project. It isn't CoreAnimation but gets the job done.

-(void)configureView{

    UITapGestureRecognizer *singleFingerTap =
    [[UITapGestureRecognizer alloc] initWithTarget:self
                                            action:@selector(handleSingleTap:)];


    _contentsView = [[UIView alloc] initWithFrame:CGRectInset(self.view.bounds, 0, 0)];
    _contentsView.backgroundColor = RGB(GRAY_BACKGROUND);
    _contentsView.layer.cornerRadius = 0;
    _contentsView.layer.masksToBounds = YES;
    [_contentsView.layer setBorderColor: [RGB(DARK_GRAY) CGColor]];
    [_contentsView.layer setBorderWidth: .5];
    [_contentsView addGestureRecognizer:singleFingerTap];
    [self.view addSubview:_contentsView];

    CardFrontController *front = [CardFrontController new];
    [front.view setFrame:CGRectMake(0, 0, 300, 300)];
    [_contentsView addSubview:front.view];

    _backView = [[UIView alloc] initWithFrame:CGRectInset(self.view.bounds, 0, 0)];

    // load the back of the card
    cardBack = [CardBackController new];
    [_backView addSubview:cardBack.view];

}

- (void)handleSingleTap:(UITapGestureRecognizer *)recognizer {

    // animate the card flipping
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.5];
    if (!_review) {
        [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:self.view cache:YES];
    } else {
        [UIView setAnimationTransition:UIViewAnimationTransitionFlipFromRight forView:self.view cache:YES];
    }
    [UIView commitAnimations];

    // now stuff that we don't need to animate
    if (!_review) {
        [_contentsView removeFromSuperview];
        [self.view addSubview:_backView];
    }
    else {
        [_backView removeFromSuperview];
        [self.view addSubview:_contentsView];
    }

    _review = !_review;
}

Upvotes: 0

Related Questions