UINavigationBar change colors on push - ios

I'm using 2 different bar tint colors at UINavigationBar in different views. I'n changing color with that method in both views:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.navigationBar.barTintColor = COLOR
}
When I tap on back button color is not changed smoothly (you can see blink on last second).
But everything is okay if just swipe view back instead of tapping on back button.
How to make smooth transition in both situations?

To achieve this kind of animation you should use UIViewControllerTransitionCoordinator as Apple documentation say it is :
An object that adopts the UIViewControllerTransitionCoordinator protocol provides support for animations associated with a view controller transition.(...)
So every UIViewController has own transitionController. To get this you should call in the UIViewControllerClass :
self.transitionCoordinator()
From documentation:
Returns the active transition coordinator object.
So to get the result that you want you should implement animateAlongsideTransition method in viewController transitionCoordinatior. Animation works when you click backButton and swipe to back.
Example :
First Controller :
class ViewControllerA: UIViewController {
override func loadView() {
super.loadView()
title = "A"
view.backgroundColor = .white
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "NEXT", style: .plain, target: self, action: #selector(self.showController))
setColors()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animate()
}
func showController() {
navigationController?.pushViewController(ViewControllerB(), animated: true)
}
private func animate() {
guard let coordinator = self.transitionCoordinator else {
return
}
coordinator.animate(alongsideTransition: {
[weak self] context in
self?.setColors()
}, completion: nil)
}
private func setColors() {
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.barTintColor = .red
}
}
Second Controller:
class ViewControllerB : UIViewController {
override func loadView() {
super.loadView()
title = "B"
view.backgroundColor = .white
setColors()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
animate()
}
override func willMove(toParentViewController parent: UIViewController?) { // tricky part in iOS 10
navigationController?.navigationBar.barTintColor = .red //previous color
super.willMove(toParentViewController: parent)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.navigationBar.barTintColor = .blue
}
private func animate() {
guard let coordinator = self.transitionCoordinator else {
return
}
coordinator.animate(alongsideTransition: {
[weak self] context in
self?.setColors()
}, completion: nil)
}
private func setColors(){
navigationController?.navigationBar.tintColor = .black
navigationController?.navigationBar.barTintColor = .blue
}
}
UPDATE iOS 10
In the iOS 10 the tricky part is to add the willMoveTo(parentViewController parent: UIViewController?) in the second ViewController. And set the navigationBar tintColor to the color value of previous controller. Also, in viewDidAppear method in second ViewControler set the navigationBar.tintColor to the color from second viewController.
Check out my example project on github

I've coded final solution that looks most comfortable to use (don't need to use a lot of overrides in own view controllers). It works perfectly at iOS 10 and easy adoptable for own purposes.
GitHub
You can check GitHub Gist for full class code and more detailed guide, I won't post full code here because Stackoverflow is not intended for storing a lot of code.
Usage
Download Swift file for GitHub. To make it work just use ColorableNavigationController instead of UINavigationController and adopt needed child view controllers to NavigationBarColorable protocol.
Example:
class ViewControllerA: UIViewController, NavigationBarColorable {
public var navigationBarTintColor: UIColor? { return UIColor.blue }
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Push", style: .plain, target: self, action: #selector(self.showController))
}
func showController() {
navigationController?.pushViewController(ViewControllerB(), animated: true)
}
}
class ViewControllerB: UIViewController, NavigationBarColorable {
public var navigationBarTintColor: UIColor? { return UIColor.red }
}
let navigationController = ColorableNavigationController(rootViewController: ViewControllerA())

This worked for me:
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
navigationController?.navigationBar.barTintColor = previous view controller's navigation bar color
}

I am just wondering. For the same purpose I use UINavigationControllerDelegate. In navigationController(_:willShow:) I start the animation using transitionCoordinator?.animate(alongsideTransition:completion:). It works great when pushing new controllers, however pop doesn't.
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
let dst = viewController as! ViewController
guard animated else {
navigationController.navigationBar.barTintColor = dst.navigationBarColor
navigationController.navigationBar.tintColor = dst.tintColor
navigationController.navigationBar.barStyle = dst.barStyle
return
}
navigationController.transitionCoordinator?.animate(alongsideTransition: { context in
navigationController.navigationBar.barTintColor = dst.navigationBarColor
navigationController.navigationBar.tintColor = dst.tintColor
navigationController.navigationBar.barStyle = dst.barStyle
}, completion: { context in
if context.isCancelled {
let source = context.viewController(forKey: UITransitionContextViewControllerKey.from) as! ViewController
navigationController.navigationBar.barTintColor = source.navigationBarColor
navigationController.navigationBar.tintColor = source.tintColor
navigationController.navigationBar.barStyle = source.barStyle
}
})
Do you see any reason why it should work with pushes but not pops?

Related

Bottom Navigation Drawer doesn't show up (Swift)

I currently working on an iOS app and I want to use a bottom navigation drawer from material-io. So I did it like it is explained in the examples on the site. But when I present the navigation Drawer the ViewController only gets a bit darker and the contentView of the drawer isn't shown.
Here is my Code:
import Foundation
import UIKit
import MaterialComponents
class CreateSubjectView: UIViewController, UITextFieldDelegate {
...
override func viewDidLoad() {
...
let bottomDrawerViewController = MDCBottomDrawerViewController()
self.modalPresentationStyle = .popover
let newViewController = self.storyboard?.instantiateViewController(withIdentifier: "TEST")
bottomDrawerViewController.contentViewController = newViewController
present(bottomDrawerViewController, animated: true, completion: nil)
...
}
...
}
Your view controller to be shown in drawer must have specified preferred content size.
Here is a demo of minimal controller. (Note: modalPresentationStyle = .popover has no effect on MDCBottomDrawerViewController)
Tested with Xcode 12
// button action in parent controller
#objc private func presentNavigationDrawer() {
let bottomDrawerViewController = MDCBottomDrawerViewController()
bottomDrawerViewController.contentViewController = DemoViewController()
present(bottomDrawerViewController, animated: true, completion: nil)
}
}
class DemoViewController: UIViewController {
override func loadView() {
super.loadView()
let view = UIView()
view.backgroundColor = .red
self.view = view
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// specify your content preferred height explicitly
self.preferredContentSize = CGSize(width: 0, height: 400) // required !!
}
#available(iOS 11.0, *)
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
// specify your content preferred height explicitly
self.preferredContentSize = CGSize(width: 0, height: 400) // required !!
}
}
Move this to viewWillAppear/ viewDidAppear once as it's too early for viewDidLoad to present a vc
class CreateSubjectView: UIViewController, UITextFieldDelegate {
let bottomDrawerViewController = MDCBottomDrawerViewController()
var once = true
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if once {
let newViewController = self.storyboard?.instantiateViewController(withIdentifier: "TEST")
bottomDrawerViewController.contentViewController = newViewController
present(bottomDrawerViewController, animated: true, completion: nil)
once = false
}
}
}

Customize Back Button for all Appearances in the App

I want to change the appearance of all Back buttons in the app by setting the text to "Back" and removing the arrow (even just removing the arrow would be fine). I'm trying to find a way to do it globally for all view controllers in the app while also keeping the functionality (I don't want to create a new instance of a UIBarButtonItem and having to set the selector).
I created a custom UINavigationController and tried to set the back button title there with navigationItem.backBarButtonItem?.title = "Back", but it didn't work. Any suggestions how to do it properly?
Just create CustomNavigationController then at
override open func pushViewController(_ viewController: UIViewController, animated: Bool) do controlClearBackTitle method
then use CustomNavigationController to fix it all in your app
import UIKit
class CustomNavigationController: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override open func pushViewController(_ viewController: UIViewController, animated: Bool) {
controlClearBackTitle()
super.pushViewController(viewController, animated: animated)
}
override open func show(_ vc: UIViewController, sender: Any?) {
controlClearBackTitle()
super.show(vc, sender: sender)
}
func controlClearBackTitle() {
self.navigationBar.backIndicatorImage = UIImage()
self.navigationBar.backIndicatorTransitionMaskImage = UIImage()
topViewController?.navigationItem.backBarButtonItem = UIBarButtonItem(title: "AnyTitle", style: .plain, target: nil, action: nil)
}
}
I think something like this should do it. Try putting it in your custom NavigationController.
let backImage = UIImage(named: "backImage")
self.navigationController?.navigationBar.backIndicatorImage = backImage
self.navigationController?.navigationBar.backIndicatorTransitionMaskImage = backImage
EDIT
Sorry totally forgot about title. This should do it:
self.navigationController?.navigationBar.backItem?.title = "New title"

iOS - Navigation Bar transparent transition

I want to make a Navigation Bar that goes transparent in detail but with my current code the Bar doesn't return to it's non transparent state. How can this be fixed? I want to this code to also work in the MoreNavigationController from the UITabBarController.
The code that has been placed in the Detail ViewController.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.transitionCoordinator?.animate(alongsideTransition: { [weak self](context) in
self?.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
self?.navigationController?.navigationBar.shadowImage = UIImage()
}, completion: { context in
})
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.transitionCoordinator?.animate(alongsideTransition: { [weak self](context) in
self?.navigationController?.navigationBar.setBackgroundImage(nil, for: UIBarMetrics.default)
self?.navigationController?.navigationBar.shadowImage = nil
}, completion: { context in
})
}
Add the code below to DetailViewController.
It was confirmed that this code also works in the UINavigationController from the UITabBarController.
NextController.swift
class NextViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setNavigationBar()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
self.navigationController?.navigationBar.shadowImage = nil
self.navigationController?.navigationBar.isTranslucent = true
}
func setNavigationBar() {
self.navigationController?.navigationBar.shadowImage = UIImage()
self.navigationController?.navigationBar.isTranslucent = false
}
}
Preview

Messages-like taller / standard navigation bar during push / pop

iOS 10 Messages app's navigation bar increases/decreases the height when you push/pop a conversation (with a smooth transition).
Typically I make a taller custom navigation bar using sizeThatFits:, but it persists across pushes and pops of view controllers in a navigation controller.
How is it possible to have a taller navigation bar just for some view controllers across navigation sequences like the Messages app?
Thanks!
Very interesting problem. I spent some time to achieve something like this in the Messages app and that is what I've done.
Finally, I use this trick to animate navigationBar height during push/pop and also pop with swipe gesture.
UIView.beginAnimations(nil, context: nil)
self.frame = navFrame
UIView.commitAnimations()
Below you can see my implementation:
extension UINavigationBar {
func applyHeight(_ height: CGFloat, animated: Bool = true) {
var navFrame = self.frame
navFrame.size.height = height
if animated {
UIView.beginAnimations(nil, context: nil)
self.frame = navFrame
UIView.commitAnimations()
} else {
self.frame = navFrame
}
}
}
class ViewControllerA: UIViewController {
override func loadView() {
super.loadView()
title = "A"
view.backgroundColor = .blue
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "NEXT", style: .plain, target: self, action: #selector(self.showController))
navigationController?.navigationBar.isTranslucent = false
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
}
func showController() {
navigationController?.pushViewController(ViewControllerB(), animated: true)
}
}
class ViewControllerB: UIViewController {
override func loadView() {
super.loadView()
title = "B"
view.backgroundColor = .red
}
override func viewWillAppear(_ animated: Bool) {
navigationController?.navigationBar.applyHeight(100)
super.viewWillAppear(animated)
}
override func willMove(toParentViewController parent: UIViewController?) {
if parent == nil { // here you know that back button was tapped
navigationController?.navigationBar.applyHeight(44)
}
super.willMove(toParentViewController: parent)
}
}
Things to improve
Title jumps to top
Jumping title is visible while you swipe to pop, but personally, I think this is a small problem :)
Hope it helps you, and maybe someone can improve this implementation. Of course, I will still try to figure out how to make this better :)
Here it's a github repository. Please use navigation_bar_height branch.
I think now you can achieve something similar with just this, just set large title to always:

Interactive view controller transition from view controller with status bar hidden

Here is my demo project.
I have two view controllers. The main one has the status bar hidden while the second one hasn't.
I created a custom driven transition animation to go from controller one to controller two.
When I'm on the child view controller (the orange one), I start the driven transition by panning from top to bottom. You can see that the status bar is coming back when dragging. And the UIButton "Hello" is moving as well.
I cancel the transition. Then I start it again and you can see the status bar is coming back as well but this time, my button isn't moving, it stays at the same location, as if the status bar was still hidden.
Any idea why it would behave like this once the transition has been cancelled at least once?
(I'm not even talking about the weird thing with the animation that is kind of doubled when cancelling (maybe a bug with the simulator as it doesn't do it on my iphone 6 9.1 and my iphone 5 8.4.)
Add: import Foundation
Then add an outlet:
class ViewController: UIViewController {
#IBOutlet weak var topConstraint: NSLayoutConstraint!
...
}
Then change the value to 0 when the view disappears and then to 20 when it will appear:
override func viewWillAppear(animated: Bool) {
topConstraint.constant = 20.0
}
override func viewWillDisappear(animated: Bool) {
topConstraint.constant = 0.0
}
Full code (make sure to remember to connect the constraint to the outlet):
import UIKit
import Foundation
class ViewController: UIViewController {
#IBOutlet weak var topConstraint: NSLayoutConstraint!
let controllerTransition = InteractiveControllerTransition(gestureType: .Pan)
let controllerTransitionDelegate = ViewController2Transition()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
controllerTransition.delegate = controllerTransitionDelegate
controllerTransition.edge = .Bottom
}
override func viewWillAppear(animated: Bool) {
topConstraint.constant = 20.0
}
override func viewWillDisappear(animated: Bool) {
topConstraint.constant = 0.0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func unwindToViewController(sender: UIStoryboardSegue) { }
override func prefersStatusBarHidden() -> Bool {
return false
}
#IBAction func helloButtonAction(sender: UIButton) {
// let storyBoard = UIStoryboard(name: "Main", bundle: nil)
// let vc = storyBoard.instantiateViewControllerWithIdentifier("ViewController2") as! ViewController2
//
// vc.transitioningDelegate = controllerTransition
// controllerTransition.toViewController = vc
//
// self.presentViewController(vc, animated: true, completion: nil)
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
// let nvc = storyBoard.instantiateViewControllerWithIdentifier("NavigationViewController2") as! UINavigationController
// let vc = nvc.topViewController as! ViewController2
let vc = storyBoard.instantiateViewControllerWithIdentifier("ViewController2") as! ViewController2
// nvc.transitioningDelegate = controllerTransition
vc.transitioningDelegate = controllerTransition
controllerTransition.toViewController = vc
// self.presentViewController(nvc, animated: true, completion: nil)
self.presentViewController(vc, animated: true, completion: nil)
}
}

Resources