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
:
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.
Hack
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.
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()
0 comments:
Post a Comment