Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom Flip Segue in Swift

Tags:

swift

xcode6

Here's my Code for my Custom Segue

class FlipFromRightSegue: UIStoryboardSegue {
override func perform() {
    let source:UIViewController = self.sourceViewController as UIViewController
    let destination:UIViewController = self.destinationViewController as UIViewController

    UIView.transitionWithView(source.view, duration: 1.0, options: .CurveEaseInOut | .TransitionFlipFromRight, animations: { () -> Void in
        source.view.addSubview(destination.view)
    }) { (finished) -> Void in
        destination.view.removeFromSuperview()
        source.presentViewController(destination, animated: false, completion: nil)
    }
}
}

I thought this works but actually the view changes only when the segue is already performed. What should I do so that the view changes when the "Flip" is in the middle?

Thanks in advance.

like image 206
borchero Avatar asked Sep 27 '14 13:09

borchero


1 Answers

As of iOS 7, we generally don't animate transitions using a custom segue. We'd either use a standard modal presentation, specifying a modalTransitionStyle (i.e. a fixed list of a few animations we can pick for our modal transitions), or you'd implement custom animation transitions. Both of those are described below:

  1. If you are simply presenting another view controller's view, the simple solution for changing the animation to a flip is by setting the modalTransitionStyle in the destination view controller. You can do this entirely in Interface Builder under the segue's properties.

    If you want to do it programmatically, in the destination controller you could do the following in Swift 3:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        modalTransitionStyle = .flipHorizontal   // use `.FlipHorizontal` in Swift 2
    }
    

    Then, when you call show/showViewController or present/presentViewController, and your presentation will be performed with horizontal flip. And, when you dismiss the view controller, the animation is reversed automatically for you.

  2. If you need more control, in iOS 7 and later, you would use custom animation transitions, in which you'd specify a modalPresentationStyle of .custom. For example, in Swift 3:

    class SecondViewController: UIViewController {
    
        let customTransitionDelegate = TransitioningDelegate()
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
    
            modalPresentationStyle = .custom   // use `.Custom` in Swift 2
            transitioningDelegate = customTransitionDelegate
        }
    
        ...
    }
    

    That specifies the UIViewControllerTransitioningDelegate that would instantiate the animation controller. For example, in Swift 3:

    class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
        func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return AnimationController(transitionType: .presenting)
        }
    
        func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
            return AnimationController(transitionType: .dismissing)
        }
    }
    

    And the animation controller would simply do .transitionFlipFromRight is a presentation, or .transitionFlipFromLeft if dismissing in Swift 3:

    class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
    
        enum TransitionType {
            case presenting
            case dismissing
        }
    
        var animationTransitionOptions: UIViewAnimationOptions
    
        init(transitionType: TransitionType) {
            switch transitionType {
            case .presenting:
                animationTransitionOptions = .transitionFlipFromRight
            case .dismissing:
                animationTransitionOptions = .transitionFlipFromLeft
            }
    
            super.init()
        }
    
        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            //let inView   = transitionContext.containerView
            let toView   = transitionContext.viewController(forKey: .to)?.view
            let fromView = transitionContext.viewController(forKey: .from)?.view
    
            UIView.transition(from: fromView!, to: toView!, duration: transitionDuration(using: transitionContext), options: animationTransitionOptions.union(.curveEaseInOut)) { finished in
                transitionContext.completeTransition(true)
            }
        }
    
        func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
            return 1.0
        }
    }
    

    For more information on the custom transitions introduced in iOS 7, see WWDC 2013 video Custom Transitions Using View Controllers.

  3. If should be acknowledged that the above AnimationController is actually a over-simplification, because we're using transform(from:to:...). That results in an animation that isn't cancelable (in case you're using interactive transition). It's also removing the "from" view itself, and as of iOS 8, that's now really the job of the presentation controller.

    So, you really want to do the flip animation using UIView.animate API. I apologize because the following involves using some unintuitive CATransform3D in key frame animations, but it results in a flip animation that can then be subjected to cancelable interactive transitions.

    So, in Swift 3:

    class AnimationController: 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
        }
    }
    
  4. FYI, iOS 8 extends the custom transition model with presentation controllers. For more information, see WWDC 2014 video A Look Inside Presentation Controllers.

    Anyway, if, at the end of the transition, the "from" view is no longer visible, you'd instruct your presentation controller to remove it from the view hierarchy, e.g.:

    class PresentationController: UIPresentationController {
        override var shouldRemovePresentersView: Bool { return true }
    }
    

    And, obviously, you have to inform your TransitioningDelegate of this presentation controller:

    class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
    
        ...
    
        func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
            return PresentationController(presentedViewController: presented, presenting: presenting)
        }            
    }
    

This answer has been updated for Swift 3. Please refer to the previous revision of this answer if you want to see the Swift 2 implementation.

like image 171
Rob Avatar answered Nov 05 '22 10:11

Rob