24

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

Visual bug

But everything is okay if just swipe view back instead of tapping on back button.

No visual bug

How to make smooth transition in both situations?

Vasily
  • 3,740
  • 3
  • 27
  • 61

4 Answers4

20

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 :

navigation_bar_animation

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

Yevhen Dubinin
  • 4,657
  • 3
  • 34
  • 57
kamwysoc
  • 6,709
  • 2
  • 34
  • 48
  • 1
    It doesn't work for me, I receive the same results as in my question (swipe back is okay, tap back is blinking). Sample project from your code: https://dl.dropboxusercontent.com/u/42855950/NavigationTransition.zip – Vasily Oct 26 '16 at 10:52
  • @Vasily I'll check it – kamwysoc Oct 26 '16 at 10:53
  • okay, please check, if you find solution update answer or remove if that is incorrect solution – Vasily Oct 26 '16 at 12:29
  • I'll let you know or remove it, Are you using swift 3 on Xcode8, right? – kamwysoc Oct 26 '16 at 12:53
  • I spent some time on this, and my previous code works fine on iOS 9 and Swift 2.2. The problem appears on iOS 10. Check my updated code and the example github [project](https://github.com/k8mil/NavBarColorAnimation) – kamwysoc Oct 26 '16 at 22:13
  • just created own solution based on some of your ideas, I think my solution will be easier to use in real application because it overrides push & pop methods and you don't need to remember previous colors in view controllers :). – Vasily Oct 27 '16 at 01:46
  • Your solution is very nice! I've focused only on providing a working solution and finding a way to fix that tricky bug in iOS 10 and finally I did that. So it would be nice if you upvote my answer or even accept. :) – kamwysoc Oct 27 '16 at 05:41
  • @kamwysoc from my testing, looks like `viewDidAppear` is not necessary for `ViewControllerB`. – Yevhen Dubinin Mar 02 '19 at 19:13
  • 1
    @kamwysoc Thanks dude!! Your iOS 10 Update helped me! thanks for saving people's time :thumbsup: – Pratik Jamariya Jun 06 '19 at 06:36
14

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())
Vasily
  • 3,740
  • 3
  • 27
  • 61
2

This worked for me:

 override func willMove(toParent parent: UIViewController?) {
      super.willMove(toParent: parent)
      navigationController?.navigationBar.barTintColor = previous view controller's navigation bar color
 }
sash
  • 8,423
  • 5
  • 63
  • 74
1

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?

push and pop

olejnjak
  • 1,163
  • 1
  • 9
  • 23
  • Did you figure it out? I'm struggling with *exactly* the same thing. – Sti Apr 24 '18 at 14:38
  • Nope, I gave up the transition and use transparent navigation bar and then in controller I create a view which simulates the background... – olejnjak Apr 24 '18 at 18:22
  • I actually made it work a couple of hours after I commented here. It was not pretty, but it works. I am now struggling with animating the UIStatusBarStyle.. – Sti Apr 24 '18 at 19:51
  • Did you make it work by overriding `willMove(toParentViewController:)` or differently? – olejnjak Apr 24 '18 at 23:22
  • 2
    Using `animateAlongside` in common superclass's willAppear. Everything worked, except popping the view with the regular back-button. Pushing worked, and swiping back worked. To fix it I subclasses navigationController, overrode `popViewController`, and set tintcolor etc there. The problem was that swiping back also triggers this function. However, navigationController has its own gestureRecognizer for the back-swipe. So before doing anything inside `popViewController` I check the state of that gesture. If the state is `possible`, then the user is not swiping back. – Sti Apr 25 '18 at 06:08