iosswiftuicontainerviewchildviewcontroller

Strange navigation bar animation when transitioning to a child UINavigationController


I have created a small project to replicate this problem.

The only file is this one...

Brief bit of code

class RootViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .red
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        showBlue()
    }

    @objc func showBlue() {
        let vc = UIViewController()
        vc.view.backgroundColor = .blue

        let nvc = UINavigationController(rootViewController: vc)

        vc.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(showGreen))

        transition(to: nvc)
    }

    @objc func showGreen() {
        let vc = UIViewController()
        vc.view.backgroundColor = .green

        let nvc = UINavigationController(rootViewController: vc)

        vc.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(showBlue))

        transition(to: nvc)
    }

    func transition(to toVC: UIViewController) {
        if let fromVC = children.first {
            transitionWithAnimation(fromVC: fromVC, toVC: toVC)
        } else {
            addWithoutAnimation(child: toVC)
        }
    }

    func addWithoutAnimation(child toVC: UIViewController) {
        addChild(toVC)
        view.addSubview(toVC.view)
        toVC.view.frame = view.bounds
        toVC.didMove(toParent: self)
    }

    func transitionWithAnimation(fromVC: UIViewController, toVC: UIViewController) {
        addChild(toVC)
        toVC.view.frame = view.bounds

        fromVC.willMove(toParent: nil)

        transition(
            from: fromVC,
            to: toVC,
            duration: 1.0,
            options: .transitionCrossDissolve,
            animations: nil) { _ in
                fromVC.removeFromParent()
                toVC.didMove(toParent: self)
        }
    }
}

Explaining the code

The RootViewController first does showBlue. This adds a child UINavigationController with a rootViewController with a blue background. The blue view controller has a Done button that then targets showGreen.

showGreen transitions to a UINavigationController with a green background and a Done button that targets showBlue.

What I expected

What I expected (and what I want to happen) is for the navigation bar to cross dissolve in place without resizing.

Animation of the problem

The problem is that during the animated transition the navigation bar has a strange animation to it. Which you can see here...

enter image description here

Apple documentation around this

All the code is followed exactly from the Apple documentation about adding child view controllers to a custom container view controller... https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/ImplementingaContainerViewController.html

Things I tried

I have also tried by using AutoLayout constraints rather than setting the view's frame directly but this didn't change anything.

I've tried running view.setNeedsLayout and then view.layoutIfNeeded() on the new view controller's view but that doesn't seem to have fixed it either.

No strange animation if child is not UINavigationController

The really odd thing is that if you use any other type of view controller (other than UINavigationController) then this animation glitch doesn't happen. For example: If one of the view controllers is a UITabBarController then the tabs do not have this odd animation. Even stranger, if the tab contains a UINavigationController then it doesn't have this animation either. It's literally just if the direct child is a UINavigationController.

Has anyone experienced this before? And did you manage to stop the strange animation?


Solution

  • If you place the transition code within a CATransaction and use the kCATransactionDisableActions key to turn off implicit actions it will resolve the issue:

        CATransaction.begin()
        CATransaction.setValue(kCFBooleanTrue, forKey:kCATransactionDisableActions)
            
        transition(
            from: fromVC,
            to: toVC,
            duration: 1.0,
            options: [.transitionCrossDissolve],
            animations: nil) { _ in
                fromVC.removeFromParent()
                toVC.didMove(toParent: self)
        }
            
        CATransaction.commit()