Animating a Navigation Bar Color

Animating a navigation bar color


Details

xCode 8.3.2, swift 3.1

Solution

override func viewWillAppear(_ animated: Bool) {
if let navigationBar = self.navigationController?.navigationBar {
navigationBar.backgroundColor = .blue
}
}

Full sample

ViewController

import UIKit

class ViewController: UIViewController {

override func viewWillAppear(_ animated: Bool) {
if let navigationBar = self.navigationController?.navigationBar {
navigationBar.barTintColor = UIColor(red: 244/255, green: 67/255, blue: 54/255, alpha: 1.0)
navigationBar.tintColor = .white
navigationBar.titleTextAttributes = [NSForegroundColorAttributeName : UIColor.white]
navigationBar.isTranslucent = false
}
}
}

ViewController2

import UIKit

class ViewController2: UIViewController {

override func viewWillAppear(_ animated: Bool) {
if let navigationBar = self.navigationController?.navigationBar {
let color = UIColor(red: 1, green: 153/255, blue: 0, alpha: 1.0)
navigationBar.setBackgroundImage(UIImage.imageWithColor(color: color), for: .default)
navigationBar.shadowImage = UIImage()
navigationBar.isTranslucent = true
}
}

override func viewWillDisappear(_ animated: Bool) {
if let navigationBar = self.navigationController?.navigationBar {
navigationBar.setBackgroundImage(nil, for: .default)
navigationBar.shadowImage = nil
navigationBar.isTranslucent = false
}
}
}

extension UIImage

import UIKit

extension UIImage {
class func imageWithColor(color: UIColor) -> UIImage {
let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContextWithOptions(CGSize(width: 1, height: 1), false, 0)
color.setFill()
UIRectFill(rect)
let image = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
}

Main.storyboard

<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16F73" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Tzy-ol-uu0">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="stackowerflow_44343355" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eIC-Nm-Ex7">
<rect key="frame" x="164" y="318" width="46" height="30"/>
<state key="normal" title="Button"/>
<connections>
<segue destination="QHs-H4-fAS" kind="show" id="Rff-Eq-K6g"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="eIC-Nm-Ex7" firstAttribute="centerX" secondItem="8bC-Xf-vdC" secondAttribute="centerX" id="G1g-VM-MAn"/>
<constraint firstItem="eIC-Nm-Ex7" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="mdZ-GP-EQw"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="hq3-zt-U4K"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="977" y="791"/>
</scene>
<!--View Controller2-->
<scene sceneID="F9C-Nz-6dd">
<objects>
<viewController id="QHs-H4-fAS" customClass="ViewController2" customModule="stackowerflow_44343355" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="AV0-X8-nhX"/>
<viewControllerLayoutGuide type="bottom" id="AsY-Gl-67v"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="1fA-pX-rzR">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Uzd-Tb-KRO" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1770" y="789"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="Jff-OO-3e7">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Tzy-ol-uu0" sceneMemberID="viewController">
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="804-YF-T6T">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="BRB-ym-7I2"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="M6i-ib-61I" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="140" y="791.15442278860576"/>
</scene>
</scenes>
</document>

Animate a UINavigationBar's barTintColor

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];

Animate navigation bar barTintColor change in iOS10 not working

To animate navigationBar’s color change in iOS10 you need to call layoutIfNeeded after setting color inside animation block.

Example code:

UIView.animateWithDuration(0.5) { 
self.navigationController?.navigationBar.barTintColor = UIColor.redColor()
self.navigationController?.navigationBar.layoutIfNeeded()
}

Also I want to inform that Apple doesn’t officialy support animations in such properties like barTintColor, so that method can break at any time.

If you call -layoutIfNeeded on the navigation bar during the animation
block it should update its background properties, but given the nature
of what these properties do, there really hasn't ever been any kind of
guarantee that you could animate any of them.

UINavigationBar color animation synchronized with push animation

These components are always very difficult to customize. I think, Apple wants system components to look and behave equally in every app, because it allows to keep shared user experience around whole iOS environment.

Sometimes, it easier to implement your own components from scratch instead of trying to customize system ones. Customization often could be tricky because you do not know for sure how components are designed inside. As a result, you have to handle lots of edge cases and deal with unnecessary side effects.

Nevertheless, I believe I have a solution for your situation. I have forked your project and implemented behavior you had described.
You can find my implementation on GitHub. See animation-implementation branch.


UINavigationBar

The root cause of pop animation does not work properly, is that UINavigationBar has it's own internal animation logic. When UINavigationController's stack changes, UINavigationController tells UINavigationBar to change UINavigationItems. So, at first, we need to disable system animation for UINavigationItems. It could be done by subclassing UINavigationBar:

class CustomNavigationBar: UINavigationBar {
override func pushItem(_ item: UINavigationItem, animated: Bool) {
return super.pushItem(item, animated: false)
}

override func popItem(animated: Bool) -> UINavigationItem? {
return super.popItem(animated: false)
}
}

Then UINavigationController should be initialized with CustomNavigationBar:

let nc = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)

UINavigationController


Since there is requirement to keep animation smooth and synchronized between UINavigationBar and presented UIViewController, we need to create custom transition animation object for UINavigationController and use CoreAnimation with CATransaction.

Custom transition

Your implementation of transition animator almost perfect, but from my point of view few details were missed. In the article Customizing the Transition Animations you can find more info. Also, please pay attention to methods comments in UIViewControllerContextTransitioning protocol.

So, my version of push animation looks as follows:

func animatePush(_ transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView

guard let toVC = transitionContext.viewController(forKey: .to),
let toView = transitionContext.view(forKey: .to) else {
return
}

let toViewFinalFrame = transitionContext.finalFrame(for: toVC)
toView.frame = toViewFinalFrame
container.addSubview(toView)

let viewTransition = CABasicAnimation(keyPath: "transform")
viewTransition.duration = CFTimeInterval(self.duration)
viewTransition.fromValue = CATransform3DTranslate(toView.layer.transform, container.layer.bounds.width, 0, 0)
viewTransition.toValue = CATransform3DIdentity

CATransaction.begin()
CATransaction.setAnimationDuration(CFTimeInterval(self.duration))
CATransaction.setCompletionBlock = {
let cancelled = transitionContext.transitionWasCancelled
if cancelled {
toView.removeFromSuperview()
}
transitionContext.completeTransition(cancelled == false)
}
toView.layer.add(viewTransition, forKey: nil)
CATransaction.commit()
}

Pop animation implementation is almost the same. The only difference in CABasicAnimation values of fromValue and toValue properties.

UINavigationBar animation

In order to animate UINavigationBar we have to add CATransition animation on UINavigationBar layer:

let transition = CATransition()
transition.duration = CFTimeInterval(self.duration)
transition.type = kCATransitionPush
transition.subtype = self.isPresenting ? kCATransitionFromRight : kCATransitionFromLeft
toVC.navigationController?.navigationBar.layer.add(transition, forKey: nil)

The code above will animate whole UINavigationBar. In order to animate only background of UINavigationBar we need to retrieve background view from UINavigationBar. And here is the trick: first subview of UINavigationBar is _UIBarBackground view (it could be explored using Xcode Debug View Hierarchy). Exact class is not important in our case, it is enough that it is successor of UIView.
Finally we could add our animation transition on _UIBarBackground's view layer direcly:

let backgroundView = toVC.navigationController?.navigationBar.subviews[0]
backgroundView?.layer.add(transition, forKey: nil)

I would like to note, that we are making prediction that first subview is a background view. View hierarchy could be changed in future, just keep this in mind.

It is important to add both animations in one CATransaction, because in this case these animations will run simultaneously.

You could setup UINavigationBar background color in viewWillAppear method of every view controller.

Here is how final animation looks like:

Sample Image

I hope this helps.

Animate Color Navigation Bar when ScrollView Down

Try setting the properties you want to animate outside of the animation block (making sure to call layoutIfNeeded first to make sure any pending layout adjustments that haven't been committed are fully layed out), then animate the layout of the navBar (instead of animating the property changes). Here's an example:

- (IBAction)animateNavBarColor:(id)sender {
[self.navigationController.navigationBar layoutIfNeeded];
self.navigationController.navigationBar.barTintColor = [UIColor blackColor];
[UIView animateWithDuration:2.0f animations:^{
[self.navigationController.navigationBar layoutIfNeeded];
}];
}

So specifically for your scenario, try adjusting your code to this:

- (void)cellScrollDownWithOffset:(CGFloat)offset {
[self.navigationController.navigationBar layoutIfNeeded];
UIColor *opacheColor = [UIColor colorWithHexString:@"#F9F9FA" setAlpha:offset /150];
UIColor *defaultColor = [UIColor colorWithWhite:1 alpha:offset /150];
self.navigationController.navigationBar.barTintColor = offset/ 150 > 0 ? opacheColor : defaultColor;
[UIView animateWithDuration:.3 animations:^{
[self.navigationController.navigationBar layoutIfNeeded];
}];
}

There's some references to animating using this method in the WWDC 2012 session: https://developer.apple.com/videos/play/wwdc2012/228/ about "Best Practices for Mastering Autolayout"

Here's another reference on the Apple Developer Forums: https://forums.developer.apple.com/thread/60258 specifically related to animating the navBar color -- specifically this portion of the response from Rincewind:

If you call -layoutIfNeeded on the navigation bar during the animation block it should update its background properties,



Related Topics



Leave a reply



Submit