Search code examples
ioscocoa-touchuiviewcore-animation

Faking UIView animation with drawRect


I have a UIView that draws itself using -drawRect: and I want to animate the colors used for the drawing. Basic UIView animation stuff doesn’t work for obvious reasons (drawRect).

I can’t simply use CAShapeLayer to draw and animate the view contents. I’d like to try faking the animation by hand, using a timer or CADisplayLink in combination with setNeedsDisplay. Is there a reasonably simple way to hide this magic behind the usual UIView animation API?

For example, let’s say there’s a color property I’d like to animate:

[UIView animateWithDuration:0.2 animations:^{
    [customDrawingView setColor:[UIColor redColor]];
}];

Is there a way to “intercept” the animation call to read the animation parameters (time, target value) and process them by hand? I don’t want to swizzle UIView.


Solution

  • I had to do something like you did quite a few times. In general you can create some classes to handle stuff like floating point interpolation or CGPoint interpolation... and do it all properly but since the whole drawing already exists there is not much point in it.

    So the UIView animateWithDuration will not work here but you can add your display link and do the interpolation manually. It is best done if no existing code is changed, only add a few methods:

    Assuming you currently have something like this:

    - (void)setColor:(UIColor *)color
    {
        _color = color;
        [self setNeedsDisplay];
    }
    

    Now you can add setColorAnimated: and do all the additional functionality:

    You need some additional parameters (properties), use what you want:

    UIColor *_sourceColor;
    UIColor *_targetColor;
    NSDate *_startDate;
    NSDate *_endDate;
    CADisplayLink *_displayLink;
    

    And the methods:

    - (void)setColorAnimated:(UIColor *)color {
        _sourceColor = self.color; // set start to current color
        _targetColor = color; // destination color
        _startDate = [NSDate date]; // begins currently, you could add some delay if you wish
        _endDate = [_startDate dateByAddingTimeInterval:.3]; // will define animation duration
    
        [_displayLink invalidate]; // if one already exists
        _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onFrame)]; // create the display link
    }
    - (UIColor *)interpolatedColor:(UIColor *)sourceColor withColor:(UIColor *)targetColor forScale:(CGFloat)scale {
        // this will interpolate between two colors
        CGFloat r1, g1, b1, a1;
        CGFloat r2, g2, b2, a2;
        [sourceColor getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
        [targetColor getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
    
        // do a linear interpolation on RGBA. You can use other
        return [UIColor colorWithRed:r1+(r2-r1)*scale
                               green:g1+(g2-g1)*scale
                                blue:b1+(b2-b1)*scale
                               alpha:a1+(a2-a1)*scale];
    }
    - (void)onFrame {
        // scale is valid between 0 and 1
        CGFloat scale = [[NSDate date] timeIntervalSinceDate:_startDate] / [_endDate timeIntervalSinceDate:_startDate];
        if(scale < .0f) {
            // this can happen if delay is used
            scale = .0f;
        }
        else if(scale > 1.0f)
        {
            // end animation
            scale = 1.0f;
            [_displayLink invalidate];
            _displayLink = nil;
        }
        [self setColor:[self interpolatedColor:_sourceColor withColor:_targetColor forScale:scale]];
    }