10

I want to make an interactive transition between a ViewController (1) and a NavigationViewController (2).

The NavigationController is called by a button, so there's no interactive transition when presenting. It can be dismissed by a button or a UIPanGestureRecognizer, so it can be dismissed interactively or not.

I have an object named TransitionManager for the transition, subclass of UIPercentDrivenInteractiveTransition.

The problem with the code below is that the two delegate methods interactionControllerFor... are never called.

Moreover, when I press the buttons or swip (UIPanGestureRecognizer), the basic animation of the modal segue is done. So the two delegate methods animationControllerFor... don't work either.

Any Ideas ? Thanks

ViewController.swift

let transitionManager = TransitionManager()

override func viewDidLoad() {
    super.viewDidLoad()

    self.transitioningDelegate = transitionManager
}

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

        let dest = segue.destinationViewController as UIViewController
        dest.transitioningDelegate = transitionManager
        dest.modalPresentationStyle = .Custom
}

TransitionManager.swift

class TransitionPushManager: UIPercentDrivenInteractiveTransition,
 UINavigationControllerDelegate, UIViewControllerTransitioningDelegate {


@IBOutlet var navigationController: UINavigationController!

var animation : Animator! // Implement UIViewControllerAnimatedTransitioning protocol


override func awakeFromNib() {
    var panGesture = UIPanGestureRecognizer(target: self, action: "gestureHandler:")
    navigationController.view.addGestureRecognizer(panGesture)

    animation = Animator()
}

func gestureHandler(pan : UIPanGestureRecognizer) {

    switch pan.state {

    case .Began :

        interactive = true

            navigationController.presentingViewController?.dismissViewControllerAnimated(true, completion:nil)


    case .Changed :

        ...            

    default :

        ...

        interactive = false

    }

}


func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return animation
}

func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return animation
}

func interactionControllerForPresentation(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return nil
}

func interactionControllerForDismissal(animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
    return self.interactive ? self : nil
}

Main.storyboard

  • The button on the ViewController triggered a modal segue to presenting the NavigationController

  • The NavigationController's delegate outlet is linked to an object of the TransitionManager class

  • The NavigationController is referenced in the TransitionManager class by the property "navigationController"

Morpheus
  • 1,189
  • 2
  • 11
  • 33

1 Answers1

21

I think the key problem is that you're configuring the transitionDelegate in viewDidLoad. That's often too late in the process. You should do it as you init the navigation controller.

Let's imagine your root scene ("Root") that presents the navigation controller scene ("Nav"), that then pushes from scene A to B to C, for example, I'd imagine an object model like this, where the navigation controller would simply own its own animation controller, interaction controller, and gesture recognizer:

view controller hierarchy and object model

This is all you need when considering (a) a custom transition (non-interactive) when "root" presents "nav"; and (b) a custom transition (interactive or not) when "nav" dismisses itself in order to return to the "root". So, I'd subclass the navigation controller which:

  • adds a gesture recognizer to its view;

  • sets the transitioningDelegate to yield the custom animation as you transition from the root scene to the navigation controller scene (and back):

  • the transitioningDelegate will also return the interaction controller (which will only exist while the gesture recognizer is in progress), yielding interactive transition during gesture and non-interactive transition if you dismiss outside of the context of the gesture.

In Swift 3, that looks like:

import UIKit
import UIKit.UIGestureRecognizerSubclass

class CustomNavigationController: UINavigationController {
    
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }
    
    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
        configure()
    }
    
    private func configure() {
        transitioningDelegate = self   // for presenting the original navigation controller
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        delegate = self                // for navigation controller custom transitions
        
        let left = UIScreenEdgePanGestureRecognizer(target: self, action: #selector(handleSwipeFromLeft(_:)))
        left.edges = .left
        view.addGestureRecognizer(left)
    }
    
    fileprivate var interactionController: UIPercentDrivenInteractiveTransition?
    
    func handleSwipeFromLeft(_ gesture: UIScreenEdgePanGestureRecognizer) {
        let percent = gesture.translation(in: gesture.view!).x / gesture.view!.bounds.size.width
        
        if gesture.state == .began {
            interactionController = UIPercentDrivenInteractiveTransition()
            if viewControllers.count > 1 {
                popViewController(animated: true)
            } else {
                dismiss(animated: true)
            }
        } else if gesture.state == .changed {
            interactionController?.update(percent)
        } else if gesture.state == .ended {
            if percent > 0.5 && gesture.state != .cancelled {
                interactionController?.finish()
            } else {
                interactionController?.cancel()
            }
            interactionController = nil
        }
    }
}

// MARK: - UINavigationControllerDelegate
//
// Use this for custom transitions as you push/pop between the various child view controllers 
// of the navigation controller. If you don't need a custom animation there, you can comment this
// out.

extension CustomNavigationController: UINavigationControllerDelegate {
    
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
        if operation == .push {
            return ForwardAnimator()
        } else if operation == .pop {
            return BackAnimator()
        }
        return nil
    }
    
    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactionController
    }
    
}

// MARK: - UIViewControllerTransitioningDelegate
//
// This is needed for the animation when we initially present the navigation controller. 
// If you're only looking for custom animations as you push/pop between the child view
// controllers of the navigation controller, this is not needed. This is only for the 
// custom transition of the initial `present` and `dismiss` of the navigation controller 
// itself.

extension CustomNavigationController: UIViewControllerTransitioningDelegate {

    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return ForwardAnimator()
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return BackAnimator()
    }

    func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactionController
    }

    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactionController
    }

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

}

// When doing custom `present`/`dismiss` that overlays the entire
// screen, you generally want to remove the presenting view controller's
// view from the view hierarchy. This presentation controller
// subclass accomplishes that for us.

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

// You can do whatever you want in the animation; I'm just fading

class ForwardAnimator : NSObject, UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
    
    func animateTransition(using context: UIViewControllerContextTransitioning) {
        let toView = context.viewController(forKey: .to)!.view!
        
        context.containerView.addSubview(toView)
        
        toView.alpha = 0.0
        
        UIView.animate(withDuration: transitionDuration(using: context), animations: {
            toView.alpha = 1.0
        }, completion: { finished in
            context.completeTransition(!context.transitionWasCancelled)
        })
    }
    
}

class BackAnimator : NSObject, UIViewControllerAnimatedTransitioning {
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.5
    }
    
    func animateTransition(using context: UIViewControllerContextTransitioning) {
        let toView   = context.viewController(forKey: .to)!.view!
        let fromView = context.viewController(forKey: .from)!.view!
        
        context.containerView.insertSubview(toView, belowSubview: fromView)
        
        UIView.animate(withDuration: transitionDuration(using: context), animations: {
            fromView.alpha = 0.0
        }, completion: { finished in
            context.completeTransition(!context.transitionWasCancelled)
        })
    }
}

So, I can just change the base class of my navigation controller in the storyboard to be this custom subclass, and now the root scene can just present the navigation controller (with no special prepare(for:)) and everything just works.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Ok, thanks a lot for your help, sorry for the errors (like for "strong", I still don't understand the difference between strong and weak...), I'm still beginning in programmation. You already helped me in another post, I said "The problem is that I don't really understand what I'm doing when using interactive transition". It's why my code is a little messy, and not really good... Your solution seems to be clean, I just have a question : I have 5 ViewControllers in my NavigationController, and I also need interactive transition between them (with push segues), is it compatible with your code ? – Morpheus Nov 01 '14 at 00:14
  • Actually, after a week of hard work, bugs and problems, I have my TransitionManager class which manage the transition : modal or push, interactive or not, presenting or dismissing. I just have a last problem (the famous "last" problem) : the transition between the first VC of the NavigationController and the Root, which is a dismissing modal transition, is not interactive. I have the problem that I explained in the other post : the interactive property is false just in the `interactionControllerForDismissal` method. – Morpheus Nov 01 '14 at 00:24
  • I'm a little afraid that change all my code brings me bugs that I will not be able to solve and nullify all my work... – Morpheus Nov 01 '14 at 00:27
  • 1
    @user3780788 If you want to try to patch your current solution to work, that's certainly your call (though I'm not going to join you in that adventure). With all due respect, though, I simply feel the design is flawed, which is why I laid out the above approach. Feel free to look at my github demo (https://github.com/robertmryan/Interactive-Custom-Transitions-in-Swift) in which I made very modest changes to the above to not only take care of the transition from root to navigation controller, but also interactive transitions as you push and pop amongst A, B, and C. – Rob Nov 01 '14 at 03:12
  • I didn't want to bother you, I saw your project on Github, it's exactly what I was looking for this week... I think it will help people. Thanks for your precious help ! – Morpheus Nov 01 '14 at 11:19