Sunday, November 19, 2017

UINavigationBar slides away instead of staying on place

Leave a Comment

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?

5 Answers

Answers 1

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) } 

Answers 2

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

Answers 3

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)     } } 

Answers 4

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)     }  } 

Answers 5

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() 
If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment