29

I am drawing a circle in the -drawRect: method of my UIView using the standard CGContextFillEllipseInRect() code. However, I would like to slightly pulse (make larger and smaller) and change the intensity of the color fill with an animation. For example, if the circle is filled with red I would like to pulse the circle and make the red slightly lighter and darker in-time with the pulsing action. Not having much experience with Core Animation I am a bit lost about how to do this, so any help would be greatly appreciated.

Skoota
  • 5,280
  • 9
  • 52
  • 75

1 Answers1

71

This is much simpler if you don't draw the circle in drawRect:. Instead, set up your view to use a CAShapeLayer, like this:

@implementation PulseView

+ (Class)layerClass {
    return [CAShapeLayer class];
}

The system sends layoutSubviews to your view whenever it changes size (including when it first appears). We override layoutSubviews to set up the shape and animate it:

- (void)layoutSubviews {
    [self setLayerProperties];
    [self attachAnimations];
}

Here's how we set the layer's path (which determines its shape) and the fill color for the shape:

- (void)setLayerProperties {
    CAShapeLayer *layer = (CAShapeLayer *)self.layer;
    layer.path = [UIBezierPath bezierPathWithOvalInRect:self.bounds].CGPath;
    layer.fillColor = [UIColor colorWithHue:0 saturation:1 brightness:.8 alpha:1].CGColor;
}

We need to attach two animations to the layer - one for the path and one for the fill color:

- (void)attachAnimations {
    [self attachPathAnimation];
    [self attachColorAnimation];
}

Here's how we animate the layer's path:

- (void)attachPathAnimation {
    CABasicAnimation *animation = [self animationWithKeyPath:@"path"];
    animation.toValue = (__bridge id)[UIBezierPath bezierPathWithOvalInRect:CGRectInset(self.bounds, 4, 4)].CGPath;
    animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    [self.layer addAnimation:animation forKey:animation.keyPath];
}

Here's how we animate the layer's fill color:

- (void)attachColorAnimation {
    CABasicAnimation *animation = [self animationWithKeyPath:@"fillColor"];
    animation.fromValue = (__bridge id)[UIColor colorWithHue:0 saturation:.9 brightness:.9 alpha:1].CGColor;
    [self.layer addAnimation:animation forKey:animation.keyPath];
}

Both of the attach*Animation methods use a helper method that creates a basic animation and sets it up to repeat indefinitely with autoreverse and a one second duration:

- (CABasicAnimation *)animationWithKeyPath:(NSString *)keyPath {
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:keyPath];
    animation.autoreverses = YES;
    animation.repeatCount = HUGE_VALF;
    animation.duration = 1;
    return animation;
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 1
    This is exactly what I'm looking for, but how do I call this from a UIView to get it to appear? I'm assuming this code is set up in its own class. – bmueller Dec 10 '12 at 22:39
  • 4
    `PulseView` is a subclass of `UIView`. – rob mayoff Dec 10 '12 at 22:48
  • What would be the modification to preserve the view final state? which is the bigger circle. setting the animation.repeatCount to one and animation.autoreverses to NO. will animate the circle to get bigger and then get smaller instantly without animation. I want the circle to stay big and not get back to its initial state. any ideas? – hasan Mar 05 '15 at 08:29
  • 1
    @hasan83 check this out http://stackoverflow.com/questions/11515647/objective-c-cabasicanimation-applying-changes-after-animation – WedgeSparda Aug 06 '15 at 11:58
  • @WedgeSparda this too old now. but thank you very much – hasan Aug 06 '15 at 12:05