7
votes

I have a custom UIView and I would like to animate its backgroundColor property. This is an animatable property of a UIView.

This is the code:

class ETTimerUIView: UIView {
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
  }
  // other methods
  func flashBg() {
    UIView.animateWithDuration( 1.0, animations: {
      self.backgroundColor = UIColor.colorYellow()
    })
  }
  override func drawRect() {
    // Something related to a timer I'm rendering
  }

This code causes causes the animation to skip and the color to change immediately:

self.backgroundColor = UIColor.colorYellow() // Changes immediately to yellow

If I animate alpha, this animates from 1 to 0 over one second as expected:

self.alpha = 0 // animates

How do I animate a background color change in this situation?

I guess I need to make a separate view--should this go in the storyboard in the same view controller? programmatically created?

Sorry, I'm really new to iOS and Swift.

3
Not sure about this, but try calling setNeedsDisplay after the call change the colorkkarayannis
No luck. The color does display, but the completion block fires immediately and switches it back (in the version that has a completion block). I think this is an animation issue.SimplGy
Where are you calling flashBg ? Can you post it too...Zell B.
I call flashBg when a timer completes, don't want to muddy the example with that, it's not important--could be called on app start, or when you tap the screen. It always never animates.SimplGy
Just tried doing let animation = CABasicAnimation(keyPath: "backgroundColor"); self.layer.addAnimation(animation, forKey: "flashBg"). Also does not work. I can animate position.x. Wish I knew what was going on. I don't know how to get any useful debug output, evenSimplGy

3 Answers

4
votes

It is indeed not working when I try it, I had a related question where putting the layoutIfNeeded() method inside the animation worked and made the view smoothly animating (move button towards target using constraints, no reaction?). But in this case, with the backgroundColor, it does not work. If someone knows the answer I will be interested to know.

But if you need a solution right now, you could create a UIView (programmatically or via the storyboard) that is used only as a container. Then you add 2 views inside : one on top, and one below, with the same frame as the container. And you only change the alpha of the top view, which let the user see the view behind :

class MyView : UIView {
    var top : UIView!

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor.blueColor()

        top = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
        top.backgroundColor = UIColor.yellowColor()
        self.addSubview(top)
    }

    override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
        let sub = UIView(frame: CGRectMake(0,0, self.frame.width, self.frame.height))
        sub.backgroundColor = UIColor.purpleColor()
        self.sendSubviewToBack(sub)
        UIView.animateWithDuration(1, animations: { () -> Void in
            self.top.alpha = 0
            }) { (success) -> Void in
                println("anim finished")
        }
    }
}
2
votes

The answer is that you cannot animate backgroundColor of a view that implements drawRect. I do not see docs for this anywhere (please comment if you know of one).

You can't animate it with animateWithDuration, nor with Core Animation.

This thread has the best explanation I've found yet:

When you implement -drawRect:, the background color of your view is then drawn into the associated CALayer, rather than just being set on the CALayer as a style property... thus prevents you from getting a contents crossfade

The solution, as @Paul points out, is to add another view above, behind, or wherever, and animate that. This animates just fine.

Would love a good understanding of why it is this way and why it silently swallows the animation instead of hollering.

1
votes

Not sure if this will work for you, but to animate the background color of a UIView I add this to a UIView extension:

extension UIView {

    /// Pulsates the color of the view background  to white.
    ///
    /// Set the number of times the animation should repeat, or pass
    /// in `Float.greatestFiniteMagnitude` to pulsate endlessly.
    /// For endless animations, you need to manually remove the animation.
    ///
    /// - Parameter count: The number of times to repeat the animation.
    ///
    func pulsate(withRepeatCount count: Float = 1) {

        let pulseAnimation = CABasicAnimation(keyPath: "backgroundColor")

        pulseAnimation.fromValue = <#source UIColor#>.cgColor
        pulseAnimation.toValue = <#target UIColor#>.cgcolor

        pulseAnimation.duration = 0.4
        pulseAnimation.autoreverses = true
        pulseAnimation.repeatCount = count

        pulseAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
        self.layer.add(pulseAnimation, forKey: "Pulsate")
        CATransaction.commit()
    }
}

When pasting this in to a source file in Xcode, replace the placeholders with your two desired colors. Or you can replace the entire lines with something like these values:

pulseAnimation.fromValue = backgroundColor?.cgColor
pulseAnimation.toValue = UIColor.white.cgColor