transitionCoordinator return nil for custom container view controller in iOS8

1.8k views Asked by At

when reference transitionCoordinator on child vc, it used to called transitionCoordinator on custom container class in iOS7, but in iOS8 this wasn't the case. Now it return nil and I have no clue what should I change to make this work.

I guess its about UIPresentationController introduced in iOS8, but can't find proper implementation for custom container view controller.

1

There are 1 answers

0
GaétanZ On

As Matt said in this previous SO question:

So, since you can't even get a transition coordinator in a situation where you are allowed to write a custom transition animation for a built-in parent view controller, obviously the chances of your getting one in a situation where you're trying to do your own parent view controller are zero

But, according to transitionCoordinator, overriding it is allowed:

Container view controllers can override this method but in most cases should not need to. If you do override this method, first call super to see if there is an appropriate transition coordinator to return, and, if there is, return it.

So, I would try to create my own coordinator for my own VC container. If you use a UIViewPropertyAnimator to manipulate the VC container's children, it's almost straightforward.

Here is an example:

class PropertyAnimatorTransitionCoordinator: NSObject, UIViewControllerTransitionCoordinator, UIViewControllerTransitionCoordinatorContext {

    private let parentView: UIView
    private let fromViewController: UIViewController?
    private let toViewController: UIViewController
    private let animator: UIViewPropertyAnimator

    // MARK: - Life Cycle

    init(parentView: UIView,
         fromViewController: UIViewController?,
         toViewController: UIViewController,
         animator: UIViewPropertyAnimator) {
        self.parentView = parentView
        self.fromViewController = fromViewController
        self.toViewController = toViewController
        self.animator = animator
    }

    // MARK: - UIViewControllerTransitionCoordinator

    func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?,
                 completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool {
        var isSuccessful = false
        if let animation = animation {
            animator.addCompletion { [weak self] _ in
                guard let context = self else { return }
                animation(context)
            }
            isSuccessful = true
        }
        if let completion = completion {
            animator.addCompletion { [weak self] _ in
                guard let context = self else { return }
                completion(context)
            }
            isSuccessful = true
        }
        return isSuccessful
    }

    func animateAlongsideTransition(in view: UIView?, animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool {
        return animate(alongsideTransition: animation, completion: completion)
    }

    func notifyWhenInteractionEnds(_ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {
        animator.addCompletion { [weak self] _ in
            guard let context = self else { return }
            handler(context)
        }
    }

    func notifyWhenInteractionChanges(_ handler: @escaping (UIViewControllerTransitionCoordinatorContext) -> Void) {
        animator.addCompletion { [weak self] _ in
            guard let context = self else { return }
            handler(context)
        }
    }

    // MARK: - UIViewControllerTransitionCoordinatorContext

    var isAnimated: Bool {
        return true
    }

    var presentationStyle: UIModalPresentationStyle {
        return .none
    }

    var initiallyInteractive: Bool {
        return false
    }

    var isInterruptible: Bool {
        return animator.isInterruptible
    }

    var isInteractive: Bool {
        return animator.isUserInteractionEnabled
    }

    var isCancelled: Bool {
        return !animator.isRunning
    }

    var transitionDuration: TimeInterval {
        return animator.duration
    }

    var percentComplete: CGFloat {
        return animator.fractionComplete
    }

    var completionVelocity: CGFloat {
        return 0
    }

    var completionCurve: UIView.AnimationCurve {
        return animator.timingParameters?.cubicTimingParameters?.animationCurve ?? .linear
    }

    var targetTransform: CGAffineTransform {
        return .identity
    }

    var containerView: UIView {
        return parentView
    }

    func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController? {
        switch key {
        case .from:
            return fromViewController
        case .to:
            return toViewController
        default:
            return nil
        }
    }

    func view(forKey key: UITransitionContextViewKey) -> UIView? {
        switch key {
        case .from:
            return fromViewController?.view
        case .to:
            return toViewController.view
        default:
            return nil
        }
    }
}

In my custom container, I would use it like this:

class CustomContainerViewController: UIViewController {

    private var customTransitionCoordinator: UIViewControllerTransitionCoordinator?

    override var transitionCoordinator: UIViewControllerTransitionCoordinator? {
        if let coordinator = super.transitionCoordinator {
            return coordinator
        }
        return customTransitionCoordinator
    }

    override var shouldAutomaticallyForwardAppearanceMethods: Bool {
        return false
    }

    func insertNewChild(_ viewController: UIViewController) {
        let animator = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut)
        customTransitionCoordinator = PropertyAnimatorTransitionCoordinator(
            parentView: view,
            fromViewController: nil,
            toViewController: viewController,
            animator: animator
        )
        animator.addCompletion { [weak self] _ in
            guard let parent = self else { return }
            viewController.didMove(toParent: parent)
            self?.customTransitionCoordinator = nil
        }
        addChild(viewController)
        viewController.beginAppearanceTransition(true, animated: true)
        view.addSubview(viewController.view)
        let target = view.bounds
        viewController.view.frame = target
        viewController.view.frame.origin.x = -target.width
        view.layoutIfNeeded()
        animator.addAnimations {
            viewController.view.frame = target
        }
        animator.addCompletion { [weak self] _ in
            guard let parent = self else { return }
            viewController.endAppearanceTransition()
            viewController.didMove(toParent: parent)
            self?.customTransitionCoordinator = nil
        }
        animator.startAnimation()
    }
}

Of course, some edge cases are not handled. It's a really basic example.