0

I've got the usual navigation bar. I'm trying to animate the color change on both title and nav buttons. For the buttons I'm using:

DispatchQueue.main.async {
            UIView.animate(withDuration: 0.5 , animations: {
                self.navigationController?.navigationBar.tintColor = color
                self.navigationController?.navigationBar.layoutIfNeeded()
            })
}

As you can see in the example below, the left back button (which is the default one) gets animated when the color changes, where as the right item (which is defined programmatically and assigned to navigationItem.rightBarButtonItem) gets the color change without any kind of animation, same for the title.

I guess the default back button can inherit from UIView, whereas UIBarButtonItem seems not, but that's my impression.

Is there a way to animate this programatically defined UIBarButtonItem ?

Example of current behaviour:

Navigation bar

Brolivar
  • 49
  • 6
  • Have you tried using a `UIView.transition` and setting options to `.transitionCrossDissolve` instead? Since you are changing black to white and white to black I think that would do the trick for you in this particular view. – shadow of arman Mar 18 '21 at 12:19
  • 1
    As far as I know `UIView.transition(with: <#UIView#>, duration: 0.5, options: .transitionCrossDissolve)` needs an UIView parameter, which `UIBarButtonItem` is not – Brolivar Mar 18 '21 at 12:37

1 Answers1

3

Your impression is correct. The UIBarButtonItem is not a view but rather instructions on how to create a view in the end.

One way you can solve this is by defining bar button item by adding it your custom view using UIBarButtonItem(customView: ). This way you can create any view and have reference to it. Then change the view color instead of bar button item color.

If this is not possible and you need to really use this property navigationBar.tintColor then you could still call the setter multiple times and doing the animation yourself.

You could for instance use timer to animate color by interpolating RGBA components of the two colors. It should look something like the following:

func animateColor(from: UIColor, to: UIColor, duration: TimeInterval, onSet: @escaping (_ interpolatedColor: UIColor) -> Void) {
    func getRGBA(_ color: UIColor) -> (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) {
        var red: CGFloat = 0
        var green: CGFloat = 0
        var blue: CGFloat = 0
        var alpha: CGFloat = 0
        color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
        return (red, green, blue, alpha)
    }
    
    let sourceComponents = getRGBA(from)
    let destinationComponents = getRGBA(to)
    
    func interpolate(_ from: CGFloat, _ to: CGFloat, _ scale: CGFloat) -> CGFloat { from + (to-from)*scale }
    
    let start: Date = .init()
    Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { timer in
        let now: Date = .init()
        
        let elapsedTime = now.timeIntervalSince(start)
        
        let scale = max(0.0, min(CGFloat(elapsedTime/duration), 1.0))
        onSet(UIColor(red: interpolate(sourceComponents.red, destinationComponents.red, scale),
                      green: interpolate(sourceComponents.green, destinationComponents.green, scale),
                      blue: interpolate(sourceComponents.blue, destinationComponents.blue, scale),
                      alpha: interpolate(sourceComponents.alpha, destinationComponents.alpha, scale)))
        
        if elapsedTime >= duration {
            // Animation done. Exit
            timer.invalidate()
        }
    }
}

And you could use it as easy as

var navigationBarTintColor: UIColor? {
    didSet {
        if let source = navigationController?.navigationBar.tintColor, let destination = navigationBarTintColor {
            animateColor(from: source, to: destination, duration: 0.3) { color in
                self.navigationController?.navigationBar.tintColor = color
            }
        } else {
            navigationController?.navigationBar.tintColor = navigationBarTintColor
        }
    }
}

There are still other things to consider such as

  • Invalidating the operation with timer when color changes twice quickly for instance.
  • Adding support for curves (ease-in, ease-out)
  • Interpolation may look nicer in HSV specter (Does not effect black-white colors)
Matic Oblak
  • 16,318
  • 3
  • 24
  • 43
  • Thank you for your answer! Just a small note, in case someone with a `ScrollView` decides to use this. The animation won’t update until you "stop" scrolling, in order to work, based on [this answer](https://stackoverflow.com/questions/34234939/swift-nstimer-stop-when-scrolling) Use this: `let timer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true)` and `RunLoop.main.add(timer, forMode: RunLoop.Mode.common)` will do the job – Brolivar Mar 19 '21 at 11:41