Animated progress view with CAGradientLayer

Modern software design is getting flatter and thinner all the time. Another trend that follows suit is the thin, one pixel progress bar that you see at the top of websites and apps. You’ve seen in it on Medium blogs, mobile Safari and other apps on iOS 7. I’m going to show you how to create a component like this to use in your own apps. Here is what we’re going to create:

Screenshot

First thing we need to do is create a new UIView subclass and give it a name. Next we need to tell this class to use CAGradientLayer as its backing layer instead of the default CALayer. You can do this by overriding the layerClass method.

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

CAGradientLayer is a pretty simple subclass of CALayer that adds a few additional properties. We’re going to be using the colors, startPoint and endPoint properties to create an animated horizontal gradient.

Now there are a couple ways to achieve this rainbow effect. One way, which I am going to use, is to create an array of UIColor objects with incremental hue values. In your initWithFrame: method add the following code:

// Use a horizontal gradient 
CAGradientLayer *layer = (id)[self layer];
[layer setStartPoint:CGPointMake(0.0, 0.5)];
[layer setEndPoint:CGPointMake(1.0, 0.5)];
 
// Create colors using hues in +5 increments
NSMutableArray *colors = [NSMutableArray array];
for (NSInteger hue = 0; hue <= 360; hue += 5) {
 
    UIColor *color;
    color = [UIColor colorWithHue:1.0 * hue / 360.0
                       saturation:1.0
                       brightness:1.0
                            alpha:1.0];
    [colors addObject:(id)[color CGColor]];
}
[layer setColors:[NSArray arrayWithArray:colors]];

Pretty straightforward. If you parent the view and run it in the simulator you’ll see our view has a horizontal gradient with all the colors in the spectrum.

Next, to create the moving effect we can cycle the colors in the colors array and use a layer animation. A single animation will move one color and repeat the process when finished. The next two methods will do the trick.

- (void)performAnimation {    
    // Move the last color in the array to the front
    // shifting all the other colors.
    CAGradientLayer *layer = (id)[self layer];    
    NSMutableArray *mutable = [[layer colors] mutableCopy];
    id lastColor = [[mutable lastObject] retain];
    [mutable removeLastObject];
    [mutable insertObject:lastColor atIndex:0];
    [lastColor release];
    NSArray *shiftedColors = [NSArray arrayWithArray:mutable];
    [mutable release];
 
    // Update the colors on the model layer
    [layer setColors:shiftedColors];
 
    // Create an animation to slowly move the gradient left to right.
    CABasicAnimation *animation;
    animation = [CABasicAnimation animationWithKeyPath:@"colors"];
    [animation setToValue:shiftedColors];
    [animation setDuration:0.08];
    [animation setRemovedOnCompletion:YES];
    [animation setFillMode:kCAFillModeForwards];
    [animation setDelegate:self];
    [layer addAnimation:animation forKey:@"animateGradient"];
}
 
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
    [self performAnimation];
}

To add the indication of progress, we can use a simple layer mask to block out portions of our gradient. Add the following two properties to your header file:

@property (nonatomic, readonly) CALayer *maskLayer;
@property (nonatomic, assign) CGFloat progress;

Be sure to @synthesize and then append the following to your initWithFrame:

maskLayer = [CALayer layer];
[maskLayer setFrame:CGRectMake(0, 0, 0, frame.size.height)];
[maskLayer setBackgroundColor:[[UIColor blackColor] CGColor]];
[layer setMask:maskLayer];

This creates a zero width mask, covering the entire view. The color of the mask doesn’t matter here but it is required to work properly. Now when our progress is updated we want to expand the width of the mask to reflect the value. Override the setProgress: method to contain the following:

- (void)setProgress:(CGFloat)value {
    if (progress != value) {
        // Progress values go from 0.0 to 1.0
        progress = MIN(1.0, fabs(value));
        [self setNeedsLayout];
    }
}
 
- (void)layoutSubviews {
    // Resize our mask layer based on the current progress
    CGRect maskRect = [maskLayer frame];
    maskRect.size.width = CGRectGetWidth([self bounds]) * progress;
    [maskLayer setFrame:maskRect];
}

Now when our progress value is set, we make sure it’s within the 0.0 to 1.0 range and invalidate the layout. Then in the next call to layoutSubviews we resize the mask based on its new value.

That’s it! You can view the entire project on GitHub.