CALayer And Auto Layout With Swift
Are you using CALayer sublayers and are you complaining about the missing auto layout? Well, let’s try some workarounds.
Introduction
Recently, I had to work with CALayer
to make some effects in an iOS Application. Unfortunately, I had some problems with the auto layout and I had to find a workaround. In this article, I propose some approaches which I tried. You can find the best one at the end of this article in “Final Comparison”.
This article is not supposed to be a guide for CALayer
but it’s just an explanation of a specific scenario: a workaround for the missing auto layout in sublayers.
Happy Reading!
What Is A CALayer?
We can consider CALayer
a graphic context of any UIView
object where we can add corners radius, borders, shadows and so on. Then, we can also apply animations to some layer properties to get nice effects—like a corner radius animation when we highlight a button.
Core animation provides several layer types by default, the main ones are:
CALayer
: it’s the base class which we can extend to create our custom layers.CATextLayer
: it’s a layer which provides the possibility to render a text from a string with the possibility to set some attributes.CAShapeLayer
: it’s a layer which provides the possibility to draw shapes using aCGPath
object.CAGradientLayer
: it’s a layer which provides the possibility to create a color gradient using an array ofCGColorRef
.
What About Auto Layout?
As we saw previously, a layer is a context of an UIView
object. It means that any UIView
object has a main layer which we can use to change its corner radius, border and so on. We don’t need to set any constraint to this main layer, since it fills automatically its view—we cannot change the frame of this layer manually since it will always fill its view.
At this point, you may be wondering: why should we bother about constraints if the main layer doesn’t need auto layout? Well, let’s consider that we want to use a sublayer in our view to add an additional text, shape or gradient with a specific frame. Unfortunately, iOS doesn’t allow the usage of constraints for sublayers. This means that we need a workaround to replace the missing auto layout.
We can use 3 approaches to achieve our goals. To simplify our code, we use a gradient layer which fills the parent view.
Please note that we can add a sublayer with whatever frame we want. We have just to set the CALayer
property frame
. In this example the sublayer fills its parent view to keep the example easy to understand.
Update In viewDidLayoutSubviews
/layoutSubviews
In this approach, we add the new gradient layer and set its frame to the parent view bounds
. Then, to keep the frame updated we must use the callback which says that the view layout has been update. If we are inside an UIViewController
we can use viewDidLayoutSubviews
, otherwise layoutSubviews
inside UIView
.
In the following example, we use the implementation inside an UIViewController
:
|
|
Update With KVO
The second approach is using KVO to observe the parent view bounds
. When bounds
changes, we manually update the layer frame to fill its parent view:
|
|
Remember to remove the observer when you use KVO.
Custom UIView
The last approach is using a custom UIView
.
We know that the main layer of any UIView
fills automatically its view, this means that, if we have a view with a specific frame, we are sure that we have also its main layer at that frame. We perfectly know that when we apply the constraints to a subview, the latter has the frame automatically updated to satisfy the constraints. At this point, we can deduce that, if we set proper constraints to a subview, we’ll have also its main layer at the specific frame since the layer is always at the same frame of its view.
The problem of this approach is that we must replace every sublayers with a custom UIView
. On the other hand, in this way we can take advantage of the constraints used for this custom view which will be applied automatically also to its main layer.
For this approach, we must create a custom UIView
class:
|
|
We said that the view has a main layer which is a CALayer
by default. For our custom view we want a main layer of type CAGradientLayer
, therefore we must override the layerClass
property which says the type of the main layer:
|
|
If we don’t override it, the main layer will be always CALayer
without any way to change its type.
And, finally, we can set our gradient in awakeFromNib
:
|
|
At the end, the class should be like this:
|
|
Final Comparison
At this point, we have 3 valid workarounds which we can use. The next step is testing the performance to understand what is the best approach. To compare them, we can create a sample project where we use all of them and check how they behave.
If you don’t want to test by yourself, you can watch the following video with the final result:
In this sample app, I added three sublayers and rotated the simulator—with slow animations enabled—to check how the layers behave when the parent view bounds
changes.
Spoiler:
As we can notice in the video, the approach with the custom UIView
performs better than the other ones. The reason is that we are relying on the auto layout applied to the view instead of updating the sublayer frame manually. Therefore, creating a custom UIView
is an acceptable trade-off to obtain the best result so far.
Conclusion
We have just seen some workarounds for the auto layout and the best approach to use. Unfortunately, it’s just a workaround and not an official approach. This means that we may find an approach which may be better than the custom UIView
. For this reason, if you have a better approach, feel free to leave a comment with a description of your solution. I would be more than happy to discuss alternatives. Thank you.