Animate a UINavigationBar's barTintColor

2019-03-10 10:23发布

问题:

The app I'm working on changes the barTintColor of its navigation bar when pushing new view controllers. Right now we set that colour in the destination view controller's viewWillAppear:method, but we have a few issues with that.

With the way we're doing this right now, the navigation bar's colour changes abruptly, while the rest of the bar content animates as usual. What I'd like is for the bar to fade between the source and destination colour. Is there any way to achieve this with public Cocoa Touch APIs?

回答1:

You can add extra animations that match the timing and animation curve of the view controller transition using UIViewControllerTransitionCoordinator.

A view controller's transitionCoordinator will be set after a view controller's animation has started (so in viewWillAppear of the presented view controller). Add any extra animations using animateAlongsideTransition:completion: on the transition coordinator.

An example:

[[self transitionCoordinator] animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
    self.navigationController.navigationBar.translucent = NO;
    self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
    self.navigationController.navigationBar.tintColor = [UIColor whiteColor];
    self.navigationController.navigationBar.barTintColor = [UIColor redColor];
} completion:nil];


回答2:

To get a smooth animation during both push and pop, I had to make the navigation bar transparent and animate my own background color view behind it.

Here's my UINavigationController subclass that handles it:

import Foundation
import UIKit

class ColorTransitionNavigationController: UINavigationController {

    var navigationBarBackgroundView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Put a background view behind the navigation bar
        navigationBarBackgroundView = UIView()
        view.insertSubview(navigationBarBackgroundView, belowSubview: navigationBar)

        // Make the navigation bar transparent
        navigationBar.isTranslucent = true
        navigationBar.setBackgroundImage(UIImage(), for: .default)

        // Size the colored background to match the navigation bar
        navigationBarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
        navigationBarBackgroundView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
        navigationBarBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
        navigationBarBackgroundView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true

        // I used a hard-coded 64 instead of constraining to the height of the navigation bar because
        // when calling navigationController.setNavigationBarHidden(true), the height of the navigation bar becomes 0
        navigationBarBackgroundView.heightAnchor.constraint(equalToConstant: 64.0).isActive = true

    }

    func setBarTintColor(color: UIColor, animated: Bool, transitionCoordinator: UIViewControllerTransitionCoordinator?) {
        guard let transitionCoordinator = transitionCoordinator, animated else {
            navigationBarBackgroundView.backgroundColor = color
            return
        }

        transitionCoordinator.animateAlongsideTransition(in: view, animation: { [weak self] (context) in

            let transition = CATransition()
            transition.duration = context.transitionDuration
            transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
            self?.navigationBarBackgroundView.layer.add(transition, forKey: nil)
            self?.navigationBarBackgroundView.backgroundColor = color

        }, completion:nil)
    }
}

Usage:

If you want a UIViewController to animate the navigation bar color when it appears, override viewWillAppear and call setBarTintColor.

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        guard let navigationController = navigationController as? ColorTransitionNavigationController else { return }
        navigationController.setBarTintColor(color: UIColor.green, animated: animated, transitionCoordinator: transitionCoordinator)
    }


回答3:

Here is a simpler fix. The issue with barTintColor not animating correctly on pop occurs when you try to set the navigation bar appearance in viewWillDisappear. The fix is to set it in willMove(toParentViewController:) instead.

The code below will produce a smooth fading transition during both push and pop, and regardless of whether it is initiated by a gesture or button tap. Tested on iOS 10 and 11.

This also works for animating barStyle.

import UIKit

class RedViewController: UIViewController {

    override func viewWillAppear(_ animated: Bool) {
        self.title = "Red"
        self.navigationController?.navigationBar.barTintColor = .red
        self.navigationController?.navigationBar.tintColor = .white
    }

    override func willMove(toParentViewController parent: UIViewController?) {
        self.navigationController?.navigationBar.barTintColor = .white
        self.navigationController?.navigationBar.tintColor = nil
    }
}