Animate Nav Bar Title Text Change

Animate nav bar title text change

If you want to animate between different title strings, use the following:

CATransition *fadeTextAnimation = [CATransition animation];
fadeTextAnimation.duration = 0.5;
fadeTextAnimation.type = kCATransitionFade;

[self.navigationController.navigationBar.layer addAnimation: fadeTextAnimation forKey: @"fadeText"];
self.navigationItem.title = "My new title";

You can adjust the duration and set a timing function to suit, of course.

There are also other types of animation that might work in different circumstances (thanks @inorganik):

kCATransitionFade
kCATransitionMoveIn
kCATransitionPush
kCATransitionReveal

Animate change to title text in Navigation Controller

So I've worked it out.

I had to find the specific layer within the UINavigationBar's subviews that contained the title text, then animate that layer. The results are exactly what I wanted.

Here's my answer (Swift 5 iOS 12)...

override func viewDidAppear(_ animated: Bool) {

super.viewDidAppear(animated)

if (newTitle ?? "").isEmpty == false { // only proceed with a valid value for newTitle.

// CATransition code
let titleAnimation = CATransition()
titleAnimation.duration = 0.5
titleAnimation.type = CATransitionType.push
titleAnimation.subtype = CATransitionSubtype.fromRight
titleAnimation.timingFunction = CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.easeInEaseOut)

// this is a detail view controller, so we must grab the reference
// to the parent view controller's navigation controller
// then cycle through until we find the title labels.
if let subviews = parent?.navigationController?.navigationBar.subviews {

for navigationItem in subviews {

for itemSubView in navigationItem.subviews {

if let largeLabel = itemSubView as? UILabel {

largeLabel.layer.add(titleAnimation, forKey: "changeTitle")
}
}
}
}
// finally set the title
navigationItem.title = newTitle
}

Note: there is no need to import QuartzCore.

GIF of iOS Simulator illustrating CATransition push fromRight

...and here's the process I went through to identify what I had to change...

This SO Q&A How to set multi line Large title in navigation bar? ( New feature of iOS 11)
helped me identify the process detailed below, so thanks in particular to the original post and the answer by @Krunal .

Using the same code to cycle through the UINavigationBar's subviews (as above), I used a print to terminal to identify the various UINavigationItems and their subviews.

        counter = 0

if let subviews = parent?.navigationController?.navigationBar.subviews {

for navigationItem in subviews {

print("____\(navigationItem)")

for itemSubView in navigationItem.subviews {

counter += 1

print("_______\(itemSubView)")
}
}
}
print("COUNTER: \(counter)")

this code yielded the following prints in terminal (for iPhone 8 Plus running iOS 12.2 in simulator)...

 ____<_UIBarBackground: 0x7f922740c000; frame = (0 -20; 414 116); userInteractionEnabled = NO; layer = <CALayer: 0x6000026ce1c0>>
_______<UIImageView: 0x7f922740c9c0; frame = (0 116; 414 0.333333); userInteractionEnabled = NO; layer = <CALayer: 0x6000026ce7c0>>
_______<UIVisualEffectView: 0x7f922740cbf0; frame = (0 0; 414 116); layer = <CALayer: 0x6000026ce880>>
____<_UINavigationBarLargeTitleView: 0x7f922740f390; frame = (0 44; 414 52); clipsToBounds = YES; layer = <CALayer: 0x6000026cd840>>
_______<UILabel: 0x7f9227499fe0; frame = (20.1667 3.66667; 206.333 40.6667); text = 'Event Details'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000005bf250>>
____<_UINavigationBarContentView: 0x7f922740d660; frame = (0 0; 414 44); clipsToBounds = YES; layer = <CALayer: 0x6000026cea00>>
_______<_UIButtonBarStackView: 0x7f92274984a0; frame = (302 0; 100 44); layer = <CALayer: 0x60000261cc60>>
_______<_UIButtonBarButton: 0x7f922749a5c0; frame = (0 0; 82.3333 44); layer = <CALayer: 0x60000261ee60>>
_______<UILabel: 0x7f922749a2d0; frame = (155 11.6667; 104.333 20.3333); text = 'Event Details'; alpha = 0; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x6000005bfc50>>
____<_UINavigationBarModernPromptView: 0x7f9227602db0; frame = (0 0; 0 50); alpha = 0; hidden = YES; layer = <CALayer: 0x6000026e4600>>
COUNTER: 2

I've actually applied the animation twice - to the UILabel layer within _UINavigationBarLargeTitleView and the UILabel layer within _UINavigationBarContentView. This does not seem to matter however because, when the large title first appears, the label within content view (which I assume is for the "old style" title in the navigation bar when the large title is scrolled off screen) is hidden on viewDidAppear.

Incidentally, if you drop in the following two lines, you'll also have multi-line large titles:

 largeLabel.numberOfLines = 0
largeLabel.lineBreakMode = .byWordWrapping

BUT, I've not yet figured out how to animate the increase in size of the large title frame, so a change to two or more lines is immediate and IMHO ruins the animation of the title change.

Not yet tested on device, but does seem to work OK for both iPhone and iPad sims.

If you find any bugs, let me know and I'll update my answer.

How can I animate the change of the navigation bar title

The title text is accessible as topItem.text. There is no way to directly access the label which is displaying this text.
So if you want to animate this label, you first have to search for it in the subview of the NavigationBar.
Then, you can apply animations on this label.
See below for an example that fades in the new title from the right.

/// Fades in the new title from the right
///
/// - Parameter newTitle: New title to display on the navigation item
func animateTitle(newTitle: String) {
// Title animation code
let titleAnimation = CATransition()
titleAnimation.duration = 0.25
titleAnimation.type = CATransitionType.push
titleAnimation.subtype = CATransitionSubtype.fromRight
titleAnimation.timingFunction = CAMediaTimingFunction.init(name: CAMediaTimingFunctionName.easeInEaseOut)

// Find the Label which contains the topitem title
if let subviews = navigationController?.navigationBar.subviews {
for navigationItem in subviews {
for itemSubView in navigationItem.subviews {
if let largeLabel = itemSubView as? UILabel {
largeLabel.layer.add(titleAnimation, forKey: "changeTitle")
}
}
}
}

navigationItem.title = newTitle
}

Transition Navigation Bar Title

You can animate the title changing by using a CATransition... however, because the title itself is a private property on the navigation bar, you need to first create a custom label and attach that to the navigation item.

Setup the title label (this would override the default navigation bar's title):

UILabel *titleLabelView = [[UILabel alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 100.0f, 44.0f) /* auto-sized anyway */];
titleLabelView.backgroundColor = [UIColor clearColor];
titleLabelView.textAlignment = NSTextAlignmentCenter;
titleLabelView.textColor = [UIColor blackColor];
titleLabelView.font = [UIFont systemFontOfSize:16.0f];
titleLabelView.adjustsFontSizeToFitWidth = YES;
titleLabelView.text = @"@cracy123";
self.navigationItem.titleView = titleLabelView;

Then whenever you want to animate the title changing (assume on a scroll view delegate action), add a CAAnimation layer and presto:

CATransition *animation = [CATransition animation];
animation.duration = 1.0;
animation.type = kCATransitionPush;
animation.subtype = kCATransitionFromTop;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[self.navigationItem.titleView.layer addAnimation:animation forKey:@"changeTitle"];

((UILabel*)self.navigationItem.titleView).text = @"JACOB K";

You can obviously change the CATransition animation properties to get the effect you're after, but those will give you the 'push-up' effect.

Sample Image

Animate title in navigation bar

You could solve it like this

var spread = document.getElementsByClassName('spread');[].forEach.call(spread, function(el) {  // replace the content width divs  el.innerHTML = '<span>' + el.innerText.split('').join('</span><span>') + '</span>'  // custom :hover  el.onmouseenter = function(e) {    var childern = e.target.childNodes    var width = e.target.offsetWidth / childern.length     for (var i = 0, child; child = childern[i]; i++) child.style.minWidth = width + 'px'  }  // remove custom style again  el.onmouseleave = function(e) {    var childern = e.target.childNodes    for (var i = 0, child; child = childern[i]; i++) child.style.minWidth = '0'  }})
.spread  {  text-align:center;}.spread span {  display: inline-block;  transition: all .5s ease;  text-align:center;  min-width: 0;}
<h1 class="spread">  Title</h1>

Navigation Bar Large Title - Animation Issue

for second issue:
in SceneDelegate: var window: UIWindow?
in the function scene put this line:

window?.backgroundColor = .yourColor

like this:

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let _ = (scene as? UIWindowScene) else { return }
window?.backgroundColor = .white
}

set the color you want and that's it :)

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>

Wrong large title animation with custom back button font

I could solve this problem on my own, but since I could not find a similar topic/problem/solution on the web I wanted to share the solution, just in case someone else has the same problem one day.

To solve this strange behavior I had to set the custom font for the normal UIControlState and additionally for the highlighted one.

UIBarButtonItem.appearance().setTitleTextAttributes([kCTFontAttributeName as NSAttributedStringKey: UIFont(name: "Futura", size: 17)!], for: .normal)

UIBarButtonItem.appearance().setTitleTextAttributes([kCTFontAttributeName as NSAttributedStringKey: UIFont(name: "Futura", size: 17)!], for: .highlighted)

Hope this helps! :)



Related Topics



Leave a reply



Submit