35

I'm setting a border width and color to a UIView subclass this way:

- (void) setViewBorder
{
    self.layer.borderColor = [UIColor greenColor].CGColor;
    self.layer.borderWidth = 3.0f;
}

My question is: How can I animate this? Thanks.

iOS Dev
  • 4,143
  • 5
  • 30
  • 58

4 Answers4

62

Both borderColor and borderWidth are animatable properties but since you are doing this in a view subclass outside of an UIView animation block, implicit animations (those that happen automatically when you change a value) are disabled.

If you want to animate these properties then you can do an explicit animation with a CABasicAnimation. Since you are animating two properties on the same layer, you can add them both to an animation group and only configure the duration, timing, etc. once. Note that the explicit animations are purely visual, the model value (the actual property) doesn't change when you add them. That is why you both configure the animation and set the model value.

CABasicAnimation *color = [CABasicAnimation animationWithKeyPath:@"borderColor"];
// animate from red to blue border ...
color.fromValue = (id)[UIColor redColor].CGColor;
color.toValue   = (id)[UIColor blueColor].CGColor;
// ... and change the model value
self.layer.borderColor = [UIColor blueColor].CGColor;

CABasicAnimation *width = [CABasicAnimation animationWithKeyPath:@"borderWidth"];
// animate from 2pt to 4pt wide border ...
width.fromValue = @2;
width.toValue   = @4;
// ... and change the model value
self.layer.borderWidth = 4;

CAAnimationGroup *both = [CAAnimationGroup animation];
// animate both as a group with the duration of 0.5 seconds
both.duration   = 0.5;
both.animations = @[color, width];
// optionally add other configuration (that applies to both animations)
both.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];

[self.layer addAnimation:both forKey:@"color and width"];

If you look at the documentation for CABasicAnimation under the section "Setting Interpolation values", you will see that it's not necessary to specify both the toValue and the fromValue like I did, so the code could be made slightly shorter. However, for clarity and readability (especially when you are starting with Core Animation) being more explicit can help you (and your coworker) understand the code.

David Rönnqvist
  • 56,267
  • 18
  • 167
  • 205
  • "since you are doing this in a view subclass outside of an UIView animation block, implicit animations (those that happen automatically when you change a value) are disabled." Is this documented anywhere? I find myself disabling layer actions a lot, didn't know about this. – jrturton Feb 21 '14 at 08:09
  • @jrturton The view is the delegate of the layer and returns `NSNull` for `actionForLayer:forKey:` when you are outside a animation block. The whole process is somewhat documented in the discussion for `actionForKey:` on CALayer. I'll see if I can find a better reference for the delegate thing. – David Rönnqvist Feb 21 '14 at 08:12
  • @jrturton In the docs for the `layer` property on UIView, you can read that the view is the delegate and that you shouldn't change the delegate. – David Rönnqvist Feb 21 '14 at 08:14
  • 2
    @jrturton As for what the view actually does in `actionForLayer:forKey:`, the best I can find [my answer here](http://stackoverflow.com/a/21240400/608157) which in turn references [this answer](http://stackoverflow.com/a/4751531/608157). However, you can make your own experiment and ask the layers delegate (or just the view directly) for `actionForLayer:forKey:` inside and outside of an animation block and see what response that you get back. I don't remember seeing it officially documented somewhere though. – David Rönnqvist Feb 21 '14 at 08:19
  • That's useful stuff. Thanks – jrturton Feb 21 '14 at 09:18
  • How do you act on animation completion, like the same way you can supply a block for `UIView`'s `animateWithDuration:animations:completion:`? – fatuhoku Apr 11 '14 at 21:24
  • @fatuhoku The short answer is that the animation object has a delegate with callbacks for start and stop. The longs answer is that this is probably a new question ;) – David Rönnqvist Apr 12 '14 at 09:18
  • Ah, yes I've sort of seen that in other snippets on SO answers relating to the topic. Thanks =] – fatuhoku Apr 13 '14 at 15:41
  • @DavidRönnqvist: did you mean `self.layer.borderColor = [UIColor blueColor].CGColor;` ? – SwiftArchitect Dec 31 '15 at 01:07
  • 1
    @SwiftArchitect yes, somehow that one remain unnoticed for a long time – David Rönnqvist Jan 03 '16 at 17:47
  • Thanks for fixing. These edits are usually rejected as *clearly conflicts with author's intent.* – SwiftArchitect Jan 12 '16 at 01:20
  • Don't forget to set the final value on the property after adding the animation to the layer. Otherwise, when the animation is finished, it reverts. – Mr Rogers May 23 '16 at 21:06
  • Animation doesn't happen. So you set animation from and to values, ok, now how do you make it START? – Brandon A Jul 26 '17 at 19:25
  • @BrandonA you add the animation object to the layer that should animate (last line) – David Rönnqvist Jul 26 '17 at 19:34
  • Yeah thats what Im doing. I am doing it in Swift. – Brandon A Jul 26 '17 at 19:44
29

Here's a swift solution for @David answer :

let colorAnimation = CABasicAnimation(keyPath: "borderColor")
colorAnimation.fromValue = UIColor.red.cgColor
colorAnimation.toValue = UIColor.blue.cgColor
view.layer.borderColor = UIColor.blue.cgColor

let widthAnimation = CABasicAnimation(keyPath: "borderWidth")
widthAnimation.fromValue = 2
widthAnimation.toValue = 4
widthAnimation.duration = 4
view.layer.borderWidth = 4

let bothAnimations = CAAnimationGroup()
bothAnimations.duration = 0.5
bothAnimations.animations = [colorAnimation, widthAnimation]
bothAnimations.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)

view.layer.add(bothAnimations, forKey: "color and width")
Brandon A
  • 8,153
  • 3
  • 42
  • 77
Meseery
  • 1,845
  • 2
  • 22
  • 19
  • 1
    You meant self.view.layer.borderColor instead of 'backgroundColor'? – John Doe Sep 08 '16 at 21:32
  • 1
    You should use `#keyPath(CALayer.borderColor)` in place of `"borderColor"`. Same with width... – Dean Kelly Oct 17 '17 at 20:41
  • @DeanKelly Why is CALayer.borderColor preferred over "borderColor"? – Eric Dec 01 '20 at 20:05
  • 1
    @Eric not 100% I remember the reasoning. I think it's more the use of the `#keyPath` portion which is compile-safe because it will fail to compile if the name of the property ever changes. – Dean Kelly Dec 02 '20 at 21:05
17

If you just want to fade in the border you can also do this:

UIView.transition(with: self, duration: 0.3, options: .transitionCrossDissolve, animations: {

        self.layer.borderColor = UIColor.green.cgColor
        self.layer.borderWidth = 3

}, completion: nil)
inf1783
  • 944
  • 11
  • 12
2

You can create a keyframe animation add it to your view.

//Create animation
        CAKeyframeAnimation *colorsAnimation = [CAKeyframeAnimation animationWithKeyPath:@"borderColor"];
        colorsAnimation.values = [NSArray arrayWithObjects: (id)[UIColor greenColor].CGColor,
                                  (id)[UIColor yellowColor].CGColor, nil];
        colorsAnimation.keyTimes = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.25],[NSNumber numberWithFloat:1.0], nil];
        colorsAnimation.calculationMode = kCAAnimationLinear;
        colorsAnimation.removedOnCompletion = NO;
        colorsAnimation.fillMode = kCAFillModeForwards;
        colorsAnimation.duration = 3.0f;

    //Create animation
    CAKeyframeAnimation *sizeAnimation = [CAKeyframeAnimation animationWithKeyPath:@"borderWidth"];
    sizeAnimation.values = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],
                             [NSNumber numberWithFloat:5.0], nil];
    sizeAnimation.keyTimes = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0],[NSNumber numberWithFloat:3.0], nil];
    sizeAnimation.calculationMode = kCAAnimationLinear;
    sizeAnimation.removedOnCompletion = NO;
    sizeAnimation.fillMode = kCAFillModeForwards;
    sizeAnimation.duration = 3.0f;

    //Add animation
    [yoursubview.layer   addAnimation:colorsAnimation forKey:nil];
    [yoursubview.layer   addAnimation:sizeAnimation forKey:nil];
CoolMonster
  • 2,258
  • 25
  • 50
  • 3
    If you are going to use removedOnCompletion = NO in an answer, then you owe it to the OP to explain the consequences. – David Rönnqvist Feb 21 '14 at 07:51
  • 1
    Also, *why* are you using a keyframe animation with only two values. It's not at all obvious when you read the code and is unnecessary complex. – David Rönnqvist Feb 21 '14 at 07:52