Question

I've set up a view such that when the app navigates forward, it performs transitionFromView using the UIViewAnimationOptionTransitionFlipFromRight option. That yields an effect like:

Using UIViewAnimationOptionTransitionFlipFromRight

Navigating backwards uses UIViewAnimationOptionTransitionFlipFromLeft, but otherwise is the same. Ok, so far, so good.

Now, I've also set up a UIPanGestureRecognizer so that I can link the flipping of views with a user's horizontal gesture on the screen. But clearly I can't use transitionFromView in that case, so my gesture recognizer is manually setting the transform property of the layer with:

CATransform3D transform = CATransform3DIdentity;
transform.m34 = 1.0 / -800.0;
viewToTransform.layer.transform = CATransform3DRotate(transform, M_PI * rotationPercent, 0.0, 1.0, 0.0);

But this is yielding a subtly different effect:

CATransform3DRotate

In the UIViewAnimationOptionTransitionFlipFromRight option of transitionFromView the view is scaled during the flipping animation such that the tallest edge of the flipped view stays the height of the container view. But when manually doing the transform of the layer, it's the center of the view that stays a constant size, slightly cropping the corners of the longer edge of the flipped view during the animation.

How do I achieve this effect when setting the transform property of the layer in my gesture recognizer (I probably want to adjust my opacity, too, to get the slight dimming effect that the transitionFromView achieves). I'm thinking I could use CATransform3DMakeScale and manually calculate the scaling function based upon the angle of the rotation and my particular m34 setting, but before I go through that exercise, I want to make sure I'm not overlooking some more intuitive or natural way to achieve this simultaneous scaling and rotating that the standard flip animation entails. And even if I have to calculate the scaling factor manually, I'd appreciate guidance as to how to define that as a function of the m34 setting and the angle (I'm no expert in the subtleties of vector transformations).

Was it helpful?

Solution

The transforms are all based around your layers anchorPoint. That is the "pivot point" around which you rotate.

The anchorPoint uses a scale from 0 to 1 in both x and y and defaults to 0.5, 0.5

I believe you would want your anchor for your layer set like this:

viewToTransform.layer.anchorPoint = CGPointMake(0.5, 1);

There is some good info and research locations in the answers to this question:

Changing my CALayer's anchorPoint moves the view

OTHER TIPS

HalR is correct, that to achieve the desired effect, one should change the anchorPoint. So for a horizontal flip rotating about the right edge of the view, one would set the anchor point as follows:

viewToTransform.layer.anchorPoint = CGPointMake(1, 0.5);

But, that will shift the view, too, so one needs to combine the new anchorPoint with CATransform3D values that translates the view back to where it originally was. For example, here is a Swift 3 custom transition "animation controller" that does a horizontal flip animation.

class HorizontalFlipAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

    enum TransitionType {
        case presenting
        case dismissing
    }

    let transitionType: TransitionType

    init(transitionType: TransitionType) {
        self.transitionType = transitionType

        super.init()
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let inView   = transitionContext.containerView
        let toView   = transitionContext.view(forKey: .to)!
        let fromView = transitionContext.view(forKey: .from)!

        var frame = inView.bounds

        func flipTransform(angle: CGFloat, offset: CGFloat = 0) -> CATransform3D {
            var transform = CATransform3DMakeTranslation(offset, 0, 0)
            transform.m34 = -1.0 / 1600
            transform = CATransform3DRotate(transform, angle, 0, 1, 0)
            return transform
        }

        toView.frame = inView.bounds
        toView.alpha = 0

        let transformFromStart:  CATransform3D
        let transformFromEnd:    CATransform3D
        let transformFromMiddle: CATransform3D
        let transformToStart:    CATransform3D
        let transformToMiddle:   CATransform3D
        let transformToEnd:      CATransform3D

        switch transitionType {
        case .presenting:
            transformFromStart  = flipTransform(angle: 0,        offset: inView.bounds.size.width / 2)
            transformFromEnd    = flipTransform(angle: -.pi,     offset: inView.bounds.size.width / 2)
            transformFromMiddle = flipTransform(angle: -.pi / 2)
            transformToStart    = flipTransform(angle: .pi,      offset: -inView.bounds.size.width / 2)
            transformToMiddle   = flipTransform(angle: .pi / 2)
            transformToEnd      = flipTransform(angle: 0,        offset: -inView.bounds.size.width / 2)

            toView.layer.anchorPoint = CGPoint(x: 0, y: 0.5)
            fromView.layer.anchorPoint = CGPoint(x: 1, y: 0.5)

        case .dismissing:
            transformFromStart  = flipTransform(angle: 0,        offset: -inView.bounds.size.width / 2)
            transformFromEnd    = flipTransform(angle: .pi,      offset: -inView.bounds.size.width / 2)
            transformFromMiddle = flipTransform(angle: .pi / 2)
            transformToStart    = flipTransform(angle: -.pi,     offset: inView.bounds.size.width / 2)
            transformToMiddle   = flipTransform(angle: -.pi / 2)
            transformToEnd      = flipTransform(angle: 0,        offset: inView.bounds.size.width / 2)

            toView.layer.anchorPoint = CGPoint(x: 1, y: 0.5)
            fromView.layer.anchorPoint = CGPoint(x: 0, y: 0.5)
        }

        toView.layer.transform = transformToStart
        fromView.layer.transform = transformFromStart
        inView.addSubview(toView)

        UIView.animateKeyframes(withDuration: self.transitionDuration(using: transitionContext), delay: 0, options: [], animations: {
            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.0) {
                toView.alpha = 0
                fromView.alpha = 1
            }
            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
                toView.layer.transform = transformToMiddle
                fromView.layer.transform = transformFromMiddle
            }
            UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.0) {
                toView.alpha = 1
                fromView.alpha = 0
            }
            UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
                toView.layer.transform = transformToEnd
                fromView.layer.transform = transformFromEnd
            }
        }, completion: { finished in
            toView.layer.transform = CATransform3DIdentity
            fromView.layer.transform = CATransform3DIdentity
            toView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
            fromView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)

            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        })
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 1.0
    }
}

That yields:

Example flip

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top