One of the typical tasks in developing applications for iOS is the creation of custom UI elements, including sometimes it may be necessary to animate changes in the value of any of the properties. The article discusses the process of creating a subclass of UIView , which has properties, the values of which can be changed with animation. A simple example: you must draw a circular progress with the ability to animate the change in color and values ranging from 0 to 1.
To create custom animations in the interfaces, the Quartz Core and Core Animation tools are used. The main work takes place in the layer classes, but in my practice, the user interfaces are usually built from the view hierarchy, so the creation of a separate UIView subclass is considered . For the same reason, we will use ARC. Let's start.
Frameworks
First of all, you need to connect the Quartz Core framework if it is not in the project. ')
Layer
Then you need to create a layer class. Let's call it TSTRoundProgressLayer and inherit from CALayer . To interact with the outside world, he will need interfaces. Let's make them in the manner of standard controls like UIProgressView :
It is worth paying attention to the storage of color in the property. Core Animation can animate a color change, but only if it is a CGColorRef . ARC does not initially understand how to store objects from the CG world, so you need to set additional memory management attributes.
Possible values of progress makes sense to limit. To do this, we need to duplicate this property in the class extension (I will explain in more detail later why). The property available outside the class will only be needed to add logic for changing values, and all work relating directly to animations will occur through its pair of extensions. Let's call it, for example, animatableProgress :
First of all, we restrict incorrect conditions. Then we narrow the drawing area a little so that the lines are not cut off by the edges of the layer (the cost of drawing lines in Core Graphics). Next, perform the calculations, setting the context and drawing directly. I draw attention to the fact that the code uses the value of the internal property animatableProgress . It is also worth noting that if the progress value is not assigned, it will be zero, and the code will work correctly. In general, if black color suits as the default progress color, progressColor can also be empty. However, if you want to set a different default color, you can use the + defaultValueForKey method:
When it comes to field names, I always recommend that you insure the combination:
NSStringFromSelector(@selector())
So when changing the name of the field, the compiler in which case it prompts you to correct it. The important point is that the drawing code fills only a small circle on the surface of the layer, and not all the allotted space entirely, so for correct drawing it is necessary that the opaque property is set to NO , or that backgroundColor has an alpha different from 1. To insure You can overload the isOpaue getter:
- (BOOL)isOpaque { returnNO; }
In addition, the drawing code itself in the example is written so that it is necessary to have the view clearsContextBeforeDrawing == YES (this is the default value).
Specifying the need to redraw when changing properties
To make the layer know that it needs to be redrawn when the property values change, you need to overload the + needsDisplayForKey method :
And so that when the property values change, the magic of CALayer works , you need to make them dynamic:
@dynamic progressColor, progress;
How it works? To manage animations, CALayer has functionality that provides work with values on keys that do not have ivars and accessors implementation. First of all, accessors are not synthesized for dynamic properties. Thus, when the accessor is called from a dynamic property (for example, -setAnimatableProgress:), the selector is not recognized, and runtime mechanisms are enabled to resolve the situation. The class + (BOOL) resolveInstanceMethod: (SEL) sel method works , in which, if the method matches the existing dynamic property of this class, it is added with the implementation that starts the animation mechanisms.
Creating animations
Finally, you need to create the animation object itself, which will be added to the layer. To do this, use the -actionForKey method :
Here, for the desired key, you must return the corresponding animation. In general, you need to return an object that satisfies the CAAction protocol, which allows the "object to respond to an action launched by CALayer" (free translation). From classes iOS SDK implements it only CAAnimation . The protocol description is rather vague, and, judging by the discussions, there is no particular sense to implement the protocol with your own hands. Moreover, CAAnimation has enough flexibility to allow to solve the vast majority of tasks associated with animations in interfaces of ordinary applications.
For the selected example, the simplest and high-level variant, CABasicAnimation, is suitable . Here we just need to specify the values for which key to animate, how long to do it and where to start. You can use more flexible types of animations and make them more fine-tuning them, depending on your needs. So, for example, I added Ising, specifying the corresponding temporary function.
It is worth noting that the animation is created before changing the value in the property, and begins to work after it, so the indication of the final value is omitted - at the time of creating the animation it is simply nowhere to take it. In turn, CABasicAnimation uses the value by keyPath at the time of the start as the initial and final default, so in this case everything works correctly.
Animations are added to the layer by calling -addAnimation: forKey:. Do not confuse key with keyPath from CABasicAnimation , since they are not directly connected. For example, adding an animation of the “foo” property can be added to the layer by the “bar” key:
When an animation itself adds a layer using -actionForKey:, it is added using the same key that changes (and is passed to this method). In this case, the code for creating an animation object is written so that the -actionForKey: key matches its keyPath . It is important that one animation can be initiated while another one is being displayed, and when adding a new animation by key, the current animation is deleted by the same key. This means that you need to think about how to smooth out the joints when one animation replaces another.
To display a layer on the screen, the system uses not the object whose property we animate, but its “presentation layer” ( presentationLayer ) - a copy created by the system. From it you can get the actual values of the properties being animated. Therefore, it is used to obtain the initial value of the animation: we start a new animation from the value that was displayed on the screen at the time of its creation.
This creates the animation object itself for the current property. It should be noted that the behavior of the layer when changing values will be the same as when changing properties like backgroundColor - by default, the change is animated. At the view level this can be changed.
External interfaces
The basis is, now you need to adjust the work of external interfaces:
As already mentioned, the layer adds animations for those keys that change. Accordingly, in order to display the changes instantly, it is enough to remove the animation by key after changing the value.
Here we see how the accessors of the external property progress are used to limit the range of values. Why do we need to add the hidden pair to the extension? If we only have the progress property, and we will overload -setProgress:,CALayer will not add its implementation method that starts the animation in runtime . I had a naive idea to overload -setValue: forKey: and add a check with a change in value, but changing the values bypasses this method, although it is invoked by the presentationLayer during the animation process. It was thought to limit the values by specifying the values when creating the animation object, but at this point the final value is not yet known. Thus, it remains only to duplicate the external property and use its hidden pair to work with animations, and external accessors to add logic.
View
Work with the layer is finished, now it needs to be wrapped in view. To do this, add a new class with similar interfaces:
Here we bring the behavior to a view that is more familiar to the view - by default, changes in values are not animated.
Using
Actually, the classes are ready to use. Now you can put the view in the hierarchy and observe the behavior when changing values. For example, you can make a controller class, in the view hierarchy of which our progress will be added. I don’t provide details of adding view, you can do it in any convenient way. I used storyboard. So, we have:
Since the content of the view is created using drawing, it will be convenient to set contentMode == UIViewContentModeRedraw , so that when the frame is changed, the content will be drawn again. This can be done in the code from the outside view or inside during initialization, or in the interface builder. Purity code for the sake of choosing the last option
A finished project with an example can be found here .