Custom Flip Segue in Swift

Custom Flip Segue in Swift

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.

How to create a custom flip horizontally push segue like the one that's used for modal segues?

I have done this several month ago.

1.Customize your transition. For example this is Push(so as Pop):

class BWFlipTransionPush: NSObject, UIViewControllerAnimatedTransitioning {

func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)! // as! UBPasswordLoginViewController
let container = transitionContext.containerView()
container!.addSubview(toVC.view)
container!.bringSubviewToFront(fromVC!.view)

//改变m34
var transfrom = CATransform3DIdentity
transfrom.m34 = -0.002
container!.layer.sublayerTransform = transfrom

//设置anrchPoint 和 position
let initalFrame = transitionContext.initialFrameForViewController(fromVC!)
toVC.view.frame = initalFrame
fromVC!.view.frame = initalFrame
toVC.view.layer.transform = CATransform3DMakeRotation(CGFloat(M_PI_2), 0, 1, 0)



//动画
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
fromVC!.view.layer.transform = CATransform3DMakeRotation(CGFloat(-M_PI_2), 0, 1, 0)
}) { (finished: Bool) -> Void in
container?.bringSubviewToFront(toVC.view)
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
toVC.view.layer.transform = CATransform3DIdentity
}) { (finished: Bool) -> Void in

fromVC!.view.layer.transform = CATransform3DIdentity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}

}
}
}

2.Set delegate for your navigation controller like this:

  override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}

3.Implement delegate function:

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .Pop:
return nil // you should return customized pop
case .Push:
return BWFlipTransionPush()
default:
return nil
}
}

Update

Here is flip pop:

class BWFlipTransionPop: NSObject, UIViewControllerAnimatedTransitioning {

func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.5
}

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)! // as! UBPasswordLoginViewController
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
let container = transitionContext.containerView()
container!.addSubview(toVC!.view)

//改变m34
var transfrom = CATransform3DIdentity
transfrom.m34 = -0.002
container!.layer.sublayerTransform = transfrom

//设置anrchPoint 和 position
let initalFrame = transitionContext.initialFrameForViewController(fromVC)
toVC!.view.frame = initalFrame
toVC!.view.layer.transform = CATransform3DMakeRotation(CGFloat(-M_PI_2), 0, 1, 0)


//动画
UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
fromVC.view.layer.transform = CATransform3DMakeRotation(CGFloat(M_PI_2), 0, 1, 0)
}) { (finished: Bool) -> Void in
container?.bringSubviewToFront(toVC!.view)
UIView.animateWithDuration(self.transitionDuration(transitionContext), delay: 0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
toVC!.view.layer.transform = CATransform3DIdentity
}) { (finished: Bool) -> Void in


fromVC.view.layer.transform = CATransform3DIdentity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled())
}

}

}
}

How to perform custom animation using segues?

Ok, I finally found all the answers here:
http://www.appcoda.com/custom-segue-animations/

Creating a custom segue is not difficult at all, as it consists of a standard programming technique in iOS. What is actually only required, is to subclass the UIStoryboardSegue class and override just one method, named perform. In this method the custom animation logic must be implemented. Triggering the transition from a view controller to another and back must also be programmatically performed by the developers, but this is a totally standard procedure.

Horizontal flip direction in iOS Storyboards

I encountered the same problem, and hopefully found a solution.

I used a custom segue to flip the way back. Here is the code of my FlipLeftSegue

@implementation FlipLeftSegue
- (void)perform
{
UIViewController *src = (UIViewController *) self.sourceViewController;
UIViewController *dst = (UIViewController *) self.destinationViewController;

[UIView beginAnimations:@"LeftFlip" context:nil];
[UIView setAnimationDuration:0.8];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:src.view.superview cache:YES];
[UIView commitAnimations];

[src presentViewController:dst animated:NO completion:nil];
}
@end

The only problem I found to this solution, is that the viewDidAppear method is called immediatly when the animation starts. Whereas with normal segue, it is called after the animation.

Slide Segue in Swift

I test with the following modification, and it work well.

func valForDirection() -> Int {
return direction == Direction.Left ? 1 : -1
}

If sliding to left, the destView's originX should be it's width. else -width.



Related Topics



Leave a reply



Submit