Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interactive Delegate Methods Never Called

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"

like image 605
Coconuts Avatar asked Oct 31 '14 17:10

Coconuts


1 Answers

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.

like image 76
Rob Avatar answered Oct 20 '22 00:10

Rob