A Conversation with Calayer – Meeting I

If you’ve been programming for iOS devices, you might have encountered these lines of code:

view.backgroundColor = [UIColor greenColor];
view.layer.cornerRadius = 8.0;
view.layer.borderWidth = 1.0;
view.layer.borderColor = [UIColor blackColor];

 

If you haven’t seen code like this before try using it with any UI component in your app and check what it does. It adds a 1pt black border to the view and round its corners by 8pt.

Every UIView is backed with a layer which can be accessed with the view.layer. The layer property points to an instance of either the CALayer class or any of its subclasses like CAShapeLayer, CAGradientLayer CATextLayer etc. Layers are part of the  Core Animation library and are extensively used in custom drawing with Core Graphics and  Core Animation, these two frameworks make iOS apps beautiful. The image below shows where these two frameworks lie in the iOS graphics drawing engine:

drawing-hierarchy

 

OpenGL, a low-level API which interacts directly with the GPU, does all the heavy lifting work of graphics processing. To make our lives even easier Apple has built Core Animation, a high-level wrapper above OpenGL so that we don’t need to write the low-level C  code.

Now, since we have been introduced to CALayer and have access to the low-level graphics rendering engine lets start interacting with them and create some beautiful graphics.

There are various  CALayer subclasses, below are the direct CALayer subclasses available in the Core Animation library:

 

calayer_hierarchy

 

In this part of the series we will build:

  1. 1. A loading indicator with CAShapeLayer and  CAKeyFrameAnimation
2. A mirror reflection of the Haptik logo with  CAReplicatorLayer
  1. 3. A colorful Haptik logo with  CATextLayer
  2. CAShapeLayer
  3. With CAShapeLayer we can easily draw curved paths and geometrical shapes. CAShapeLayer is mostly used in drawing custom UI components. At Haptik, we design most of the custom UIButton subclasses with CAShapeLayer.

    But why would you write code to draw a circle or a triangle or anything complex when you have images?

  4.  

21634-trollface-why

There are a few reasons to write code:

  • 1. Almost all properties of shape layers are animatable, which gives us the freedom to        change these shapes with code at runtime, which we’ll learn how to do.
  •  
  • 2. CAShapeLayer is vector based, thus they are resolution independent.
  •  
  • 3. This can be drawn directly using the GPU so we can free the CPU for other tasks.
  •  

By the end of this section, we’ll be able to show this cool animation on the screen:

 

cashapelayer

The animation above has three components:

  1. 1. A rounded rectangle added on the view layer
  2. 2. A gray color circle added on the rectangle layer
  3. 3. A dark gray color arc added on the circle layer
  4.  

The below code draws the rounded rectangle on the screen:

CAShapeLayer *roundedRect = [CAShapeLayer layer];
roundedRect.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 120, 120) cornerRadius:8.0].CGPath;
roundedRect.fillColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5].CGColor;
[self.view.layer addSublayer:roundedRect];

 

To create the path of the shape layer we used a UIKit class UIBezierPath to skip the complexity of drawing paths with Core Graphics. The fillColor property of the shape layer fills the closed region of the layer with given color.

Next, we add a circle to the rounded rectangle:

CAShapeLayer *circle = [CAShapeLayer layer];
    
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0, 0) radius:50 startAngle:0.0*(M_PI/180.0)  endAngle:360.0*(M_PI/180.0) clockwise:YES].CGPath;
circle.lineWidth = 5.0;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor lightGrayColor].CGColor;
circle.backgroundColor = [UIColor clearColor].CGColor;
circle.position = CGPointMake(60, 60);
    
[roundedRect addSublayer:circle];

 

For drawing a circle we need to pass a startAngle and an endAngle. With these angles, we tell the system from where the path should start and where it should be drawn till. If we were drawing this circle with a pen, consider the strokeColor as the ink color and the lineWidth as the minimum width of the line that can be drawn with the pen. Changing the position of the layer centers it in the rectangle.

To add the arc we will again use the same function for drawing a circle, but we will pass different start angles and end angles to draw it as an arc:

CAShapeLayer *arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0, 0) radius:50 startAngle:180.0*(M_PI/180.0)  endAngle:225.0*(M_PI/180.0) clockwise:YES].CGPath;
arc.lineWidth = 5.0;
arc.lineCap = kCALineCapRound;
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor darkGrayColor].CGColor;
arc.backgroundColor = [UIColor clearColor].CGColor;

 

The lineCap determines how the endpoints of the drawn curve are stroked.

To create a rotational animation in x-y plane we need to change the rotation transform along the z-axis and fortunately, we can easily do this with Core Animation.

 

CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.rotation.z"];
animation.additive = YES;
animation.duration = 10.0;
animation.repeatCount = HUGE_VALF;
animation.values = @[
                    [NSNumber numberWithFloat:0.0 * M_PI],
                    [NSNumber numberWithFloat:1.75 * M_PI],
                    [NSNumber numberWithFloat:-0.75 * M_PI],
                    [NSNumber numberWithFloat:2.75 * M_PI],
                    [NSNumber numberWithFloat:0.0 * M_PI]
                    ];
animation.keyTimes = @[ @0, @(2 / 6.0), @(3 / 6.0), @(5 / 6.0), @1 ];
animation.timingFunctions = @[
                             [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn],
                             [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                             [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                             [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]
                             ];
[arc addAnimation:animation forKey:@"rotate"];

 

With CAKeyFrameAnimation we can control the animation attributes like fromValue and toValue timingFunction calculationMode for different time intervals in the complete animation. The values array determines fromValue and  toValue of the animatable property ( transform.rotation.z) in the time intervals given to the keyTimes array. The timing functions decide how the animations start and end.

You can find the complete code till this section on this CALayers-GitHub repo.

 

CAReplicatorLayer

 

CAReplicatorLayer is a container layer, it replicates the content added to it. It has some cool properties which can be used to instruct the container how the replication has to be done. Beautiful effects can be achieved by applying animations to the replicated content. Every contained content is called an instance. To show the usage of this layer we will create a reflection of an image. By the end of this section, we’ll be able to show the reflection of the Haptik logo like this:

 

haptik-hand

Let’s build this!

First, we need a CAReplicatorLayer instance and on this instance, we’ll be adding an image layer of which the reflection we will be showing:

// Create a CAReplicatorLayer
CAReplicatorLayer *replicatorLayer = [CAReplicatorLayer layer];
 
// Create the image layer
UIImage *image = [UIImage imageNamed:@"haptik_logo"];
CALayer *imageLayer = [CALayer layer];
imageLayer.contents = (__bridge id)[image CGImage];
imageLayer.bounds = CGRectMake(0.0, 0.0, [image size].width, [image size].height);
imageLayer.anchorPoint = CGPointMake(0, 0);
 
// Set bounds of replicator layer
// to height twice of image height
replicatorLayer.bounds = CGRectMake(0.0, 0.0, [image size].width, [image size].height * 2);
replicatorLayer.masksToBounds = YES;
replicatorLayer.anchorPoint = CGPointMake(0.5, 0.0);
replicatorLayer.position = CGPointMake(self.view.frame.size.width / 2.0, 80.0);    
[replicatorLayer addSublayer:imageLayer];

 

This code is pretty straight forward, the anchorPoint of a layer is the point from where all the geometric manipulations will happen. The default              anchorPoint is  (0.5, 0.5)  which represents the center of the layer. We want to apply a rotation from the top of the layer, so we changed it to (0,0).

With the above code, we have added an image to the replicator layer and set its correct bounds. To get the reflection we need to apply a rotation transform and translate the replicated layer to the correct position as below:

CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DScale(transform, 1.0, -1.0, 1.0);
transform = CATransform3DTranslate(transform, 0.0, -[image size].height * 2.0, 1.0);
replicatorLayer.instanceTransform = transform;
replicatorLayer.instanceCount = 2;

 

The instanceTransform property of the replicator layer allows us to set the calculated transform on the replicated content. There are other properties of the replicator layer like instanceDelay instanceColor which can be manipulated to get more control.

Setting the instanceCount to 2 instructs the replicator layer to create exactly two instances of the added content.

This is it! Running this code will give us the below output:

 

haptik-hand-mirrored

But this is not what you expected, yes because the mirror we used earlier was blurred and so was the reflection. But if that is what you also need then add a gradient layer to your layer as shown below:

CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.colors = @[
                           (__bridge id)[[[UIColor whiteColor] colorWithAlphaComponent:0.25] CGColor],
                           (__bridge id)[[UIColor whiteColor] CGColor]
                           ];
 
gradientLayer.bounds = CGRectMake(0.0, 0.0, replicatorLayer.frame.size.width, [image size].height + 1.0);
 
gradientLayer.position = CGPointMake(replicatorLayer.position.x, replicatorLayer.position.y + [image size].height * 1.5);

 

At Haptik, we have used the CAReplicatorLayer to create a new typing indicator. This is how it looks!

typing

If you want to download and run this code check the Github repo. And yes, Craig Federighi was online. 😉

 

CATextLayer

 

Text layers are used to layout and render plain and attributed strings, but we do this usually with UILabel. One amazing usage of CATextLayer is to mask UIView. In this section we will redesign the Haptik logo as in the image below:

color-text-logo

We create a UIImageView with a pattern image and mask that pattern with the text layer:

// Create the imageView
UIImage *haptikLogo = [UIImage imageNamed:@"Artboard"];
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
imageView.image = haptikLogo;
 
// Create the CATextLayer instance.
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = imageView.bounds;
textLayer.rasterizationScale = [UIScreen mainScreen].scale;
textLayer.contentsScale = [UIScreen mainScreen].scale;

 

Never forget to set the rasterizationScale and  contentsScale, without these properties you might get blurry or smaller text depending on the screen resolution of the devices your app runs on.

Set whatever string you want to display as a mask with the desired font:

textLayer.fontSize = 100.0;
textLayer.font = (__bridge CFTypeRef _Nullable)([UIFont systemFontOfSize:100]);
textLayer.string = @"haptik";

 

Finally, use the text layer as the mask on the image view and we are done:

imageView.layer.mask = textLayer;

 

Build and run the app and see how it looks like.

The app source code till this section can be downloaded from CALayer-Github repo. Looking forward to hearing from you all.

Also, don’t forget, we are hiring high-quality engineers. So if you are interested reach out to us at hello@haptik.ai.