User95797654974
User95797654974

Reputation: 644

UINavigationController glitchy animation when pushing new view controller

I want both the welcome view controller and ViewController2 to have a UIVisualEffectView for their background. When I push ViewController2, the animation is very glitchy and doesn't fully animate to the left during the push animation. Pop animation works fine. Is there anyway to fix this weird animation?

Minimum Reproducible Example is below

class ViewController: UIViewController {
    override func loadView() {
        view = MKMapView()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
    
    override func viewDidAppear(_ animated: Bool) {
        let welcomeVC = WelcomeViewController()
        let navigationController = UINavigationController(rootViewController: welcomeVC)
        navigationController.isModalInPresentation = true
        if let sheet = navigationController.sheetPresentationController {
            sheet.detents = [.custom { context in
                return 200
            }, .medium(), .large()]
            sheet.prefersGrabberVisible = true
            sheet.preferredCornerRadius = 15
            sheet.largestUndimmedDetentIdentifier = .large
        }
        
        present(navigationController, animated: true)
    }
}

class WelcomeViewController: UIViewController {
    override func viewDidLoad() {
        let blurEffect = UIBlurEffect(style: .systemThinMaterial)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = view.bounds
        view.addSubview(blurEffectView)
        view.sendSubviewToBack(blurEffectView)
        
                
        let button = UIButton()
        button.setTitle("Go to View Controller 2", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 200),
            button.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    @objc func buttonTapped() {
        let vc2 = ViewController2()
        navigationController?.pushViewController(vc2, animated: true)
    }
}

class ViewController2: UIViewController {
    override func viewDidLoad() {
        let blurEffect = UIBlurEffect(style: .systemThinMaterial)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = view.bounds
        view.addSubview(blurEffectView)
        view.sendSubviewToBack(blurEffectView)
        
        
        let button = UIButton()
        button.setTitle("Go back to Welcome VC", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 200),
            button.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    @objc func buttonTapped() {
        
        navigationController?.popViewController(animated: true)
    }
}

Upvotes: 0

Views: 78

Answers (1)

DonMag
DonMag

Reputation: 77672

When using a UINavigationController, the default transition is not a direct "slide side-by-side." The controller views overlap and fade.

You have a couple options...

  • use a custom transition for your navigation controller
  • embed a UIPageViewController
  • use a UIScrollView as a "container" and load your view controllers as children

Which approach to use depends on a number of factors -- such as how many controllers you want to show, do you want interactive drag-to-slide, etc.

Probably the most straightforward approach would be the "container" approach.

Here's a complete, minimal example, based on your code:

class ExampleViewController: UIViewController {
    override func loadView() {
        view = MKMapView()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        let containerVC = ExampleContainerVC()
        containerVC.isModalInPresentation = true
        if let sheet = containerVC.sheetPresentationController {
            sheet.detents = [.custom { context in
                return 200
            }, .medium(), .large()]
            sheet.prefersGrabberVisible = true
            sheet.preferredCornerRadius = 15
            sheet.largestUndimmedDetentIdentifier = .large
        }
        present(containerVC, animated: true)
    }
}

protocol ContainerNavDelegate: AnyObject {
    func showV2()
    func showWelcome()
}

class ExampleContainerVC: UIViewController, ContainerNavDelegate {

    let scrollView = UIScrollView()
    let stackView = UIStackView()
    
    override func viewDidLoad() {

        let blurEffect = UIBlurEffect(style: .systemThinMaterial)
        let blurEffectView = UIVisualEffectView(effect: blurEffect)
        blurEffectView.frame = view.bounds
        view.addSubview(blurEffectView)
        
        scrollView.showsVerticalScrollIndicator = false
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.isScrollEnabled = false

        stackView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(stackView)
        
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(scrollView)
        
        let g = view.safeAreaLayoutGuide
        let cg = scrollView.contentLayoutGuide
        let fg = scrollView.frameLayoutGuide
        
        NSLayoutConstraint.activate([
            
            scrollView.topAnchor.constraint(equalTo: g.topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            
            stackView.topAnchor.constraint(equalTo: cg.topAnchor),
            stackView.leadingAnchor.constraint(equalTo: cg.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: cg.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: cg.bottomAnchor),
            stackView.heightAnchor.constraint(equalTo: fg.heightAnchor),
            
        ])
        
        let vc1 = WelcomeVC()
        addChild(vc1)
        stackView.addArrangedSubview(vc1.view)
        vc1.view.widthAnchor.constraint(equalTo: fg.widthAnchor).isActive = true
        vc1.didMove(toParent: self)

        let vc2 = SecondVC()
        addChild(vc2)
        stackView.addArrangedSubview(vc2.view)
        vc2.view.widthAnchor.constraint(equalTo: fg.widthAnchor).isActive = true
        vc2.didMove(toParent: self)

        vc1.navDelegate = self
        vc2.navDelegate = self
        
    }

    func showV2() {
        guard stackView.arrangedSubviews.count == 2 else { return }
        let v = stackView.arrangedSubviews[1]
        scrollView.scrollRectToVisible(v.frame, animated: true)
    }
    
    func showWelcome() {
        guard stackView.arrangedSubviews.count > 0 else { return }
        let v = stackView.arrangedSubviews[0]
        scrollView.scrollRectToVisible(v.frame, animated: true)
    }

}

class WelcomeVC: UIViewController {
    
    var navDelegate: ContainerNavDelegate?
    
    override func viewDidLoad() {

        view.backgroundColor = .clear

        let button = UIButton()
        button.setTitle("Go to View Controller 2", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 200),
            button.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    @objc func buttonTapped() {
        navDelegate?.showV2()
    }
}

class SecondVC: UIViewController {
    
    var navDelegate: ContainerNavDelegate?
    
    override func viewDidLoad() {

        view.backgroundColor = .clear

        let button = UIButton()
        button.setTitle("Go back to Welcome VC", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
        button.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(button)
        
        NSLayoutConstraint.activate([
            button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            button.widthAnchor.constraint(equalToConstant: 200),
            button.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
    
    @objc func buttonTapped() {
        navDelegate?.showWelcome()
    }
}

Upvotes: 0

Related Questions