4

I created demo project to show the problem.

We have two view controllers inside UINavigationController.

MainViewController which is the root.

class MainViewController: UIViewController {

    lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("Detail", for: .normal)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Main"
        view.backgroundColor = .blue
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.widthAnchor.constraint(equalToConstant: 150).isActive = true
        button.heightAnchor.constraint(equalToConstant: 42).isActive = true
        button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
    }

    @objc func buttonTapped(_ sender: UIButton) {
        navigationController?.pushViewController(DetailViewController(), animated: true)
    }
}

And DetailViewController which is pushed.

class DetailViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        navigationController?.setNavigationBarHidden(true, animated: animated)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        navigationController?.setNavigationBarHidden(false, animated: animated)
    }
}

As you can see I want to hide UINavigationBar in DetailViewController:

Demo

Question

The problem is that, UINavigationBar slides away instead of stay of his place together with whole MainViewController. How can I change that behavior and keep pop gesture?

Nominalista
  • 3,669
  • 7
  • 32
  • 80
  • 3
    It's a Navigation Controller stack and managed by the `navigationController`. – Mannopson Nov 10 '17 at 13:03
  • How it's related to the question? How this information will help me to change behavior of UINavigationBar which slides away instead of staying on place with MainViewController? – Nominalista Nov 10 '17 at 13:11
  • Do you want the NavBar to animate away *during* the view transition? You *could* try hiding it in `viewDidAppear` instead... that should give you the animation, but it will be *after* the new view has slid into place. – DonMag Nov 10 '17 at 13:18
  • Exactly, it will stay until animation of DetailViewController ends, which is horrible. The NavigationBar should stay like the content below, I don't know why Apple developers changed that to slides outside the content... – Nominalista Nov 10 '17 at 13:25
  • @ThirdMartian I think you should create a custom presentation. – Mannopson Nov 10 '17 at 13:28
  • What you mean by custom presentation? – Nominalista Nov 10 '17 at 13:35
  • @ThirdMartian It takes a lot of work and time. You can create your own presentation style. Check [this](http://mathewsanders.com/custom-menu-transitions-in-swift/) out – Mannopson Nov 10 '17 at 13:40
  • @ThirdMartian - If I'm looking at the same transition in the Twitter app that you want to emulate, it's very possible that the new view is *not* being pushed onto the NavController. Or, if it *is* being pushed, it's using a custom presentation. As an aside - I think it is terrible UX... I just tried some repeated tapping, it it shows me the same two alternating views, but I have to hit the "back" arrow a dozen times to get "back" to where I was. – DonMag Nov 10 '17 at 13:46
  • @DonMag notice that you can use gesture to get back. What do you mean by repeated tapping? What it shows? – Nominalista Nov 10 '17 at 13:53
  • I understood the question wrong in the first place. But I think this may be what you want https://stackoverflow.com/a/5660278/7270113 You can easily create a custom push animation that is like what I think you described using these few lines of code. – erik_m_martens Nov 10 '17 at 13:59
  • @erikmartens but notice that you can't use gesture, in Twitter app you can. – Nominalista Nov 10 '17 at 14:01
  • @ThirdMartian - List of tweets... tap the "user name / icon"... tap the first tweet... tap the "user name / icon" (and see the previous view, but it has been pushed onto the stack again)... tap the first tweet and it is **again** pushed onto the stack... tap the "user name / icon" and ***again*** the previous view is pushed... ad infinitum. Do that for a while, and there are dozens of the same views on the stack, and it's next to impossible to get back to the start. – DonMag Nov 10 '17 at 14:03
  • I think it has nothing in common with the question, but imho you can always tap on Home indicator and get back to root. – Nominalista Nov 10 '17 at 14:05
  • @ThirdMartian This will just change how the animation looks like. Anything else is left unchanged. If you want no gesture, my guess would be to present instead of push the viewcontroller. The animation should look like a push still, as you added a custom animation. If you present the view controller you will also not have to hide the navigation controller because you just don't embed it into one before you present. – erik_m_martens Nov 10 '17 at 14:08
  • @erikmartens I think it's not working with pop gesture as it should, the problem looks the same. Can you show the code to this project how it could work? – Nominalista Nov 10 '17 at 14:11
  • @ThirdMartian I'll test it in a small test project and then post it here – erik_m_martens Nov 10 '17 at 14:18
  • @ThirdMartian So I finally could test it and found out the following. For pushing view controllers, you can replace the transition animation as detailed in the stack overflow post that I posted above. For presenting view controllers it is indeed more cumbersome as pointed out by Mannopson. But I can help you to get rid of the gesture and still push the view controller. Check out my edited answer below. – erik_m_martens Nov 10 '17 at 18:03
  • if answer is helpful then make it as right. – Sagar Bhut Aug 30 '18 at 12:34

5 Answers5

4

in your MainViewController add that method

override func viewDidAppear(_ animated: Bool) {        
        UIView.animate(withDuration: 0) {
            self.navigationController?.setNavigationBarHidden(false, animated: false)
        }
    }

and replace your method with below method in DetailViewController

 override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    navigationController?.setNavigationBarHidden(true, animated: animated)
}
Sagar Bhut
  • 659
  • 6
  • 24
1

The following code is hacking.

override func viewDidAppear(_ animated: Bool) {        
    UIView.animate(withDuration: 0) {
        self.navigationController?.setNavigationBarHidden(false, animated: false)
    }
}

Do not write this bizarre code, as suggested by @sagarbhut in his post (in this thread).

You have two choices.

  1. Hack

  2. Do not hack.

Use convenience functions like this one

https://developer.apple.com/documentation/uikit/uiview/1622562-transition

Create a custom segue, if you are using storyboards.

https://www.appcoda.com/custom-segue-animations/

Implement the UIViewControllerAnimatedTransitioning protocol

https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

You can get some great results but I'm afraid you will need to work hard. There are numerous tutorials online that discuss how to implement the above.

enter image description here

0

Twitter's navigation transition where the pushed ViewController's view seems to take the entire screen "hiding the navigationBar", but still having the pop gesture animation and the navigationBar visible in the pushing ViewController even during the transition animation obviously cannot be achieved by setting the bar's hidden property.

Implementing a custom navigation system is one way to do it but I suggest a simple solution by playing on navigationBar's layer and its zPosition property. You need two steps,

  • Set the navigationBar's layer zPosition to a value that'd place it under its siblings which include the current visible view controller's view in the navigation stack: navigationController?.navigationBar.layer.zPosition = -1

    The pushing VC's viewDidLoad could be a good place to do that.

  • Now that the navigationBar is placed behind the VC's view, you'll need to adjust the view's frame to make sure it doesn't overlap with the navigationBar (that'd cause navigationBar to be covered). You can use viewWillLayoutSubviews to change the view's origin.y to start under navigationBar's floor (statusBarHeight + navigationBarHeight).

That'll do the job. You don't need to modify the pushed VC unless you wanna add e.g. a custom back button like in the Twitter's profile screen case. The detail controller's view will be on top of navigation bar while letting you keep the pop gesture transition. Below is your sample code modified with this changes:

class MainViewController: UIViewController {

    lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("Detail", for: .normal)
        button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)

        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Main"
        view.backgroundColor = .blue

        // Default value of layer's zPosition is 0 so setting it to -1 will place it behind its siblings.
        navigationController?.navigationBar.layer.zPosition = -1

        // The `view` will be under navigationBar so lets set a background color to the bar
        // as the view's backgroundColor to simulate the default behaviour.
        navigationController?.navigationBar.backgroundColor = view.backgroundColor

        // Hide the back button transition image.
        navigationController?.navigationBar.backIndicatorImage = UIImage()
        navigationController?.navigationBar.backIndicatorTransitionMaskImage = UIImage()

        view.addSubview(button)
        addConstraints()
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        // Place `view` under navigationBar.
        let statusBarPlusNavigationBarHeight: CGFloat = (navigationController?.navigationBar.bounds.height ?? 0) 
          + UIApplication.shared.statusBarFrame.height
        let viewHeight = UIScreen.main.bounds.height - statusBarPlusNavigationBarHeight
        view.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: viewHeight))
        view.frame.origin.y = statusBarPlusNavigationBarHeight
    }

    @objc func buttonTapped(_ sender: UIButton) {
        navigationController?.pushViewController(DetailViewController(), animated: true)
    }

    private func addConstraints() {
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.widthAnchor.constraint(equalToConstant: 150).isActive = true
        button.heightAnchor.constraint(equalToConstant: 42).isActive = true
    }
}

class DetailViewController: UIViewController {

    // Some giant button to replace the navigationBar's back button item :)
    lazy var button: UIButton = {
        let b: UIButton = UIButton(frame: CGRect(origin: .zero, size: CGSize(width: 80, height: 40)))
        b.frame.origin.y = UIApplication.shared.statusBarFrame.height
        b.backgroundColor = .darkGray
        b.setTitle("back", for: .normal)
        b.addTarget(self, action: #selector(DetailViewController.backButtonTapped), for: .touchUpInside)
        return b
    }()

    @objc func backButtonTapped() {
        navigationController?.popViewController(animated: true)
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white

        view.addSubview(button)
    }
}
Lukas
  • 3,311
  • 2
  • 12
  • 24
-1

This might be what you're looking for...

Start the NavBar hide / show animations before starting the push / pop:

class MainViewController: UIViewController {

    lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("Detail", for: .normal)
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Main"
        view.backgroundColor = .blue
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.widthAnchor.constraint(equalToConstant: 150).isActive = true
        button.heightAnchor.constraint(equalToConstant: 42).isActive = true
        button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
    }

    @objc func buttonTapped(_ sender: UIButton) {
        navigationController?.setNavigationBarHidden(true, animated: true)
        navigationController?.pushViewController(DetailViewController(), animated: true)
    }
}

class DetailViewController: UIViewController {

    lazy var button: UIButton = {
        let button = UIButton()
        button.setTitle("Go Back", for: .normal)
        button.backgroundColor = .red
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .white
        view.addSubview(button)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
        button.widthAnchor.constraint(equalToConstant: 150).isActive = true
        button.heightAnchor.constraint(equalToConstant: 42).isActive = true
        button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
    }

    @objc func buttonTapped(_ sender: UIButton) {
        navigationController?.setNavigationBarHidden(false, animated: true)
        navigationController?.popViewController(animated: true)
    }

}
DonMag
  • 44,662
  • 5
  • 32
  • 56
  • 1
    The result is that UINavigationBar slides to top and it's visible on DetailViewController for a milliseconds. Not looking good. Look at Twitter app when you tap on user. – Nominalista Nov 10 '17 at 13:34
  • OK - I will try to look at the Twitter app (don't currently use it) to see what effect you are going for. – DonMag Nov 10 '17 at 13:36
-2

Use the custom push transition from this post stackoverflow.com/a/5660278/7270113. The in order to eliminate the back gesture (that's what I understand is what you want to do), just kill the navigation stack. You will have to provide an alternative way to exit the DetailViewController, as even if you unhide the navigation controller, the backbitten will be gone since the navigation stack is empty.

@objc func buttonTapped(_ sender: UIButton) {

    let transition = CATransition()
    transition.duration = 0.5
    transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    transition.type = kCATransitionFade
    navigationController?.view.layer.add(transition, forKey: nil)

    let storyboard = UIStoryboard(name: "NameOfYourStoryBoard", bundle: .main)
    let viewController = storyboard.instantiateViewController(withIdentifier: "IdentifierOfDetailViewController") as! DetailViewController
    navigationController?.setViewControllers([viewController], animated: true) // This method will perform a push
}

Your navigation controller will from now on use this transition animation, if you want to remove it you could use

navigationController?.view.layer.removeAllAnimations()
erik_m_martens
  • 479
  • 3
  • 7