Chapter 4. Animation

Animation is an attribute changing over time. In general, this will usually be a visible attribute of something in the interface. The changing attribute might be positional: something moves or changes size, not jumping abruptly, but sliding smoothly. Other kinds of attribute can animate as well. A view’s background color might change from red to green, not switching colors abruptly, but fading from one to the other. A view might change from opaque to transparent, not vanishing abruptly, but fading away.

Without help, most of us would find animation beyond our reach. There are just too many complications — complications of calculation, of timing, of screen refresh, of threading, and many more. Fortunately, help is provided. You don’t perform an animation yourself; you describe it, you order it, and it is performed for you. You get animation on demand.

Asking for an animation can be as simple as setting a property value; under some circumstances, a single line of code will result in animation:

myLayer.backgroundColor = UIColor.red.cgColor // animate to red

Animation is easy because Apple wants to facilitate your use of it. Animation isn’t just cool and fun; it clarifies that something is changing or responding. It is crucial to the character of the iOS interface.

For example, one of my first apps was based on a macOS game in which the user clicks cards to select them. In the macOS version, a card was highlighted to show it was selected, and the computer would beep to indicate a click on an ineligible card. On iOS, these indications were insufficient: the highlighting felt weak, and you can’t use a sound warning in an environment where the user might have the volume turned off or be listening to music. So in the iOS version, animation is the indicator for card selection (a selected card waggles eagerly) and for tapping on an ineligible card (the whole interface shudders, as if to shrug off the tap).

The purpose of this chapter is to explain the basics of animation itself; how you use it is another matter. Using animation effectively, especially in relation to touch (the subject of the next chapter), is a deep and complex subject involving psychology, biology, and other fields outside mere programming. Apple’s own use of animation, for example, is extraordinary deep and pervasive. It is used to make the interface feel live, fluid, responsive, intuitive, and natural. It helps to provide the user with a sense of what the user can do and is doing, of where the user is, of how things on the screen are related. Many WWDC videos go into depth about what Apple achieves with animation, and these can assist you in your own design.

Drawing, Animation, and Threading

Animation is based on an interesting fact about how iOS draws to the screen: drawing doesn’t actually take place at the time you give your drawing commands. When you give a command that requires a view to be redrawn, the system remembers your command and marks the view as needing to be redrawn. Later, when all your code has run to completion and the system has, as it were, a free moment, then it redraws all views that need redrawing. Let’s call this the redraw moment. (I’ll explain what the redraw moment really is later in this chapter.)

Animation works the same way, and is part of the same process. When you ask for an animation to be performed, the animation doesn’t start happening on the screen until the next redraw moment. (You can force an animation to start immediately, but this is unusual.) Like a movie (especially an old-fashioned animated cartoon), an animation has “frames.” An animated value does not change smoothly and continuously; it changes in small, individual increments that give the illusion of smooth, continuous change. This illusion works because the device itself undergoes a periodic, rapid, more or less regular screen refresh — a constant succession of redraw moments — and the incremental changes are made to fall between these refreshes. Apple calls the system component responsible for this the animation server.

Think of the “animation movie” as being interposed between the user and the “real” screen. While the animation lasts, this movie is superimposed onto the screen. When the animation is finished, the movie is removed, revealing the state of the “real” screen behind it. The user is unaware of all this, because (if you’ve done things correctly) at the time that it starts, the movie’s first frame looks just like the state of the “real” screen at that moment, and at the time that it ends, the movie’s last frame looks just like the state of the “real” screen at that moment.

So, when you animate a view’s movement from position 1 to position 2, you can envision a typical sequence of events like this:

  1. You reposition the view. The view is now set to position 2, but there has been no redraw moment, so it is still portrayed at position 1.

  2. You order an animation of the view from position 1 to position 2.

  3. The rest of your code runs to completion.

  4. The redraw moment arrives. If there were no animation, the view would now suddenly be portrayed at position 2. But there is an animation, and so the “animation movie” appears. It starts with the view portrayed at position 1, so that is still what the user sees.

  5. The animation proceeds, each “frame” portraying the view at intermediate positions between position 1 and position 2. (The documentation describes the animation as now in-flight.)

  6. The animation ends, portraying the view ending up at position 2.

  7. The “animation movie” is removed, revealing the view indeed at position 2 — where you put it in the first step.

Realizing that the “animation movie” is different from what happens to the real view is key to configuring an animation correctly. A frequent complaint of beginners is that a position animation is performed as expected, but then, at the end, the view “jumps” to some other position. This happens because you set up the animation but failed to move the view to match its final position in the “animation movie”; when the “movie” is whipped away at the end of the animation, the real situation that’s revealed doesn’t match the last frame of the “movie,” so the view appears to jump.

There isn’t really an “animation movie” in front of the screen — but it’s a good analogy, and the effect is much the same. In reality, it is not a layer itself that is portrayed on the screen; it’s a derived layer called the presentation layer. Thus, when you animate the change of a view’s position or a layer’s position from position 1 to position 2, its nominal position changes immediately; meanwhile, the presentation layer’s position remains unchanged until the redraw moment, and then changes over time, and because that’s what’s actually drawn on the screen, that’s what the user sees.

(A layer’s presentation layer can be accessed through its presentation method — and the layer itself may be accessed through the presentation layer’s model method. I’ll give examples, in this chapter and the next, of situations where accessing the presentation layer is a useful thing to do.)

The animation server operates on an independent thread. You don’t have to worry about that fact (thank heavens, because multithreading is generally rather tricky and complicated), but you can’t ignore it either. Your code runs independently of and possibly simultaneously with the animation — that’s what multithreading means — so communication between the animation and your code can require some planning.

Arranging for your code to be notified when an animation ends is a common need. Most of the animation APIs provide a way to set up such a notification. One use of an “animation ended” notification might be to chain animations together: one animation ends and then another begins, in sequence. Another use is to perform some sort of cleanup. A very frequent kind of cleanup has to do with handling of touches: what a touch means while an animation is in-flight might be quite different from what a touch means when no animation is taking place.

Since your code can run even after you’ve set up an animation, or might start running while an animation is in-flight, you need to be careful about setting up conflicting animations. Multiple animations can be set up (and performed) simultaneously, but trying to animate or change a property that’s already in the middle of being animated may be an incoherency. You’ll want to take care not to let your animations step on each other’s feet accidentally.

Outside forces can interrupt your animations. The user might click the Home button to send your app to the background, or an incoming phone call might arrive while an animation is in-flight. The system deals coherently with this situation by simply canceling all in-flight animations when an app is backgrounded; you’ve already arranged before the animation for your views to assume the final states they will have after the animation, so no harm is done — when your app resumes, everything is in that final state you arranged beforehand. But if you wanted your app to resume an animation in the middle, where it left off when it was interrupted, that would require some canny coding on your part.

Image View and Image Animation

UIImageView provides a form of animation so simple as to be scarcely deserving of the name; still, sometimes it might be all you need. You supply the UIImageView with an array of UIImages, as the value of its animationImages or highlightedAnimationImages property. This array represents the “frames” of a simple cartoon; when you send the startAnimating message, the images are displayed in turn, at a frame rate determined by the animationDuration property, repeating as many times as specified by the animationRepeatCount property (the default is 0, meaning to repeat forever), or until the stopAnimating message is received. Before and after the animation, the image view continues displaying its image (or highlightedImage).

For example, suppose we want an image of Mars to appear out of nowhere and flash three times on the screen. This might seem to require some sort of Timer-based solution, but it’s far simpler to use an animating UIImageView:

let mars = UIImage(named: "Mars")!
let empty = UIGraphicsImageRenderer(size:mars.size).image {_ in}
let arr = [mars, empty, mars, empty, mars]
let iv = UIImageView(image:empty)
iv.frame.origin = CGPoint(100,100)
self.view.addSubview(iv)
iv.animationImages = arr
iv.animationDuration = 2
iv.animationRepeatCount = 1
iv.startAnimating()

You can combine UIImageView animation with other kinds of animation. For example, you could flash the image of Mars while at the same time sliding the UIImageView rightward, using view animation as described in the next section.

UIImage supplies a form of animation parallel to that of UIImageView: an image can itself be an animated image. Just as with UIImageView, this means that you’ve prepared multiple images that form a sequence serving as the “frames” of a simple cartoon. You can create an animated image with one of these UIImage class methods:

animatedImage(with:duration:)

As with UIImageView’s animationImages, you supply an array of UIImages. You also supply the duration for the whole animation.

animatedImageNamed(_:duration:)

You supply the name of a single image file, as with init(named:), with no file extension. The runtime appends "0" (or, if that fails, "1") to the name you supply and makes that image file the first image in the animation sequence. Then it increments the appended number, gathering images and adding them to the sequence (until there are no more, or we reach "1024").

animatedResizableImageNamed(_:capInsets:resizingMode:duration:)

Combines an animated image with a resizable image (Chapter 2).

You do not tell an animated image to start animating, nor are you able to tell it how long you want the animation to repeat. Rather, an animated image is always animating, repeating its sequence once every duration seconds, so long as it appears in your interface; to control the animation, add the image to your interface or remove it from the interface, possibly exchanging it for a similar image that isn’t animated.

An animated image can appear in the interface anywhere a UIImage can appear as a property of some interface object. In this example, I construct a sequence of red circles of different sizes, in code, and build an animated image which I then display in a UIButton:

var arr = [UIImage]()
let w : CGFloat = 18
for i in 0 ..< 6 {
    let r = UIGraphicsImageRenderer(size:CGSize(w,w))
    arr += [r.image { ctx in
        let con = ctx.cgContext
        con.setFillColor(UIColor.red.cgColor)
        let ii = CGFloat(i)
        con.addEllipse(in:CGRect(0+ii,0+ii,w-ii*2,w-ii*2))
        con.fillPath()
    }]
}
let im = UIImage.animatedImage(with:arr, duration:0.5)
b.setImage(im, for:.normal) // b is a button in the interface
Warning

Images are memory hogs, and an array of images can cause your app to run completely out of memory. Confine your use of image view and image animation to a few small images.

View Animation

All animation is ultimately layer animation, which I’ll discuss later in this chapter. However, for a limited range of properties, you can animate a UIView directly: these are its alpha, bounds, center, frame, transform, and (if the view doesn’t implement draw(_:)) its backgroundColor. You can also animate a UIView’s change of contents. In addition, the UIVisualEffectView effect property is animatable between nil and a UIBlurEffect; and, starting in iOS 11, a view’s underlying layer’s cornerRadius is animatable under view animation as well. This list of animatable features, despite its brevity, will often prove quite sufficient.

A Brief History of View Animation

The view animation API has evolved historically by way of three distinct major stages. Older stages have not been deprecated or removed; all three stages are present simultaneously:

Begin and commit

Way back at the dawn of iOS time, a view animation was constructed imperatively using a sequence of UIView class methods. To use this API, you call beginAnimations, configure the animation, set an animatable property, and commit the animation by calling commitAnimations. For example:

UIView.beginAnimations(nil, context: nil)
UIView.setAnimationDuration(1)
self.v.backgroundColor = .red
UIView.commitAnimations()
Block-based animation

When Objective-C blocks were introduced in iOS 4, the entire operation of configuring a view animation was reduced to a single UIView class method, to which you pass a block in which you set an animatable property. In Swift, an Objective-C block is a function — usually an anonymous function. We can call this the animations function:

UIView.animate(withDuration:1) {
    self.v.backgroundColor = .red
}
Property animator

iOS 10 introduced a new object — a property animator (UIViewPropertyAnimator). It, too, receives an animations function in which you set an animatable property:

let anim = UIViewPropertyAnimator(duration: 1, curve: .linear) {
    self.v.backgroundColor = .red
}
anim.startAnimation()

Although begin-and-commit animation still exists, you’re unlikely to use it. Block-based animation completely supersedes it — except in one special situation where you want an animation that repeats a specific number of times (I’ll demonstrate later in this chapter).

The property animator does not supersede block-based animation; rather, it supplements and expands it. There are certain kinds of animation (repeating animation, autoreversing animation, transition animation) where a property animator can’t help you, and you’ll go on using block-based animation. But for the bulk of basic view animations, the property animator brings some valuable advantages — a full range of timing curves, multiple completion functions, and the ability to pause, resume, reverse, and interact by touch with a view animation.

Property Animator Basics

The UIViewPropertyAnimator class derives some of its methods and properties from its protocol inheritance. It adopts the UIViewImplicitlyAnimating protocol, which itself adopts the UIViewAnimating protocol. (The reason for this division of powers won’t arise in this chapter; it has to do with custom view controller transition animations, discussed in Chapter 6.)

Here’s an overview of UIViewPropertyAnimator’s inheritance:

UIViewAnimating protocol

As a UIViewAnimating protocol adopter, UIViewPropertyAnimator can have its animation started with startAnimation, paused with pauseAnimation, and stopped with stopAnimation(_:) plus finishAnimation(at:). Its state property reflects its current state (UIViewAnimatingState) — .inactive, .active, or .stopped — and its isRunning property distinguishes whether it is .active but paused. UIViewAnimating also provides two settable properties:

fractionComplete

Essentially, the current “frame” of the animation.

isReversed

Dictates whether the animation is running forward or backward.

UIViewImplicitlyAnimating protocol

As a UIViewImplicitlyAnimating protocol adopter, UIViewPropertyAnimator can be given completion functions to be executed when the animation finishes, with addCompletion(_:). It can also be given additional animations functions, with addAnimations(_:) or addAnimations(_:delayFactor:); animations defined by multiple animations functions are combined additively (I’ll explain later what that means).

UIViewImplicitlyAnimating also provides a continueAnimation(withTimingParameters:durationFactor:) method that allows a paused animation to be resumed with altered timing and duration; the durationFactor is the desired fraction of the animation’s original duration, or zero to mean whatever remains of the original duration.

UIViewPropertyAnimator

UIViewPropertyAnimator’s own methods consist solely of initializers; I’ll explain how to initialize a property animator later, when I talk about timing curves. It has some read-only properties describing how it was configured and started (for example, reporting its animation’s duration). UIViewPropertyAnimator also provides five settable properties:

isInterruptible

If true (the default), the animator can be paused or stopped.

isUserInteractionEnabled

If true (the default), animated views can be tapped midflight.

scrubsLinearly

If true (the default), then when the animator is paused, the animator’s animation curve is temporarily replaced with a linear curve.

isManualHitTestingEnabled

If true, hit-testing is up to you; the default is false, meaning that the animator performs hit-testing on your behalf, which is usually what you want. (See Chapter 5 for more about hit-testing animated views.)

pausesOnCompletion

If true, then when the animation finishes, it does not revert to .inactive; the default is false.

As you can see, a property animator comes packed with power for controlling the animation after it starts. You can pause the animation in mid-flight, allow the user to manipulate the animation gesturally, resume the animation, reverse the animation, and much more. I’ll illustrate all those features in this and subsequent chapters. In the simplest case, however, you’ll just launch the animation and stand back, as I demonstrated earlier:

let anim = UIViewPropertyAnimator(duration: 1, curve: .linear) {
    self.v.backgroundColor = .red
}
anim.startAnimation()

In that code, the UIViewPropertyAnimator object anim is instantiated as a local variable, and we are not retaining it in a persistent property; yet the animation works because the animation server retains it. We can keep a persistent reference to the property animator if we’re going to need it elsewhere, and I’ll give examples later showing how that can be a useful thing to do.

Here’s how a property animator’s states work. At the moment the property animator is started with startAnimation, it transitions through state changes, as follows:

  1. The animator starts life in the .inactive state.

  2. When startAnimation is called, the animator immediately enters the .active state with isRunning set to false (paused).

  3. The animator then immediately transitions again to the .active state with isRunning set to true.

The “animation movie” doesn’t start running until the next redraw moment. Once the animation is set in motion, it continues to its finish and then runs through those same states in reverse:

  1. The running animator was in the .active state with isRunning set to true.

  2. When the animation finishes, the animator switches to .active with isRunning set to true (paused).

  3. The animator then immediately transitions back to the .inactive state.

When the animator finishes and reverts to the .inactive state, it jettisons its animations. This means that the animator, if you’ve retained it, is reusable after finishing only if you supply new animations. However, if you set the animator’s pausesOnCompletion to true, the third step is omitted; the animation comes to an end without the animator transitioning back to the .inactive state, and ultimately stopping the animator is then up to you.

To stop an animator, send it the stopAnimation(_:) message. The animator then enters the special .stopped state. Typically, you will then call finishAnimation(at:), after which the animator returns to .inactive. The stopAnimation(_:) parameter is a Bool signifying whether you want to dispense with finishAnimation(at:) and let the runtime clean up for you.

It is a runtime error to let an animator go out of existence while paused (.active but isRunning is false) or stopped (.stopped). Your app will crash unceremoniously if you allow that to happen. If you pause an animator, you must call stopAnimation(true), or else call stopAnimation(false) followed by finishAnimation(at:), thus bringing it back to the .inactive state in good order, before the animator goes out of existence.

View Animation Basics

Any animatable change made within an animations function will be animated, so we can, for example, animate simultaneous changes both in the view’s color and in its position:

let anim = UIViewPropertyAnimator(duration: 1, curve: .linear) {
    self.v.backgroundColor = .red
    self.v.center.y += 100
}
anim.startAnimation()

You can add an animations function to a property animator after instantiating it; indeed, the init(duration:timingParameters:) initializer actually requires that you do this, as it lacks an animations: parameter. Thus a property animator can end up with multiple animations functions:

let anim = UIViewPropertyAnimator(duration: 1,
    timingParameters: UICubicTimingParameters(animationCurve:.linear))
anim.addAnimations {
    self.v.backgroundColor = .red
}
anim.addAnimations {
    self.v.center.y += 100
}
anim.startAnimation()

A completion function, which can be added to a property animator with the addCompletion(_:) method, lets us specify what should happen after the animation ends. As with the animations functions, a property animator can be assigned more than one completion function; the completion functions are executed in the order in which they were added:

var anim = UIViewPropertyAnimator(duration: 1, curve: .linear) {
    self.v.backgroundColor = .red
    self.v.center.y += 100
}
anim.addCompletion {_ in
    print("hey")
}
anim.addCompletion {_ in
    print("ho")
}
anim.startAnimation() // animates, finishes, then prints "hey" and "ho"

Changes not only to multiple properties but even to multiple views can be combined into a single animations function. In this way, elaborate effects can be combined into a single animation.

For example, suppose we want to make one view dissolve into another. We start with the second view present in the view hierarchy, with the same frame as the first view, but with an alpha of 0, so that it is invisible. Then we animate the change of the first view’s alpha to 0 and the second view’s alpha to 1. Indeed, we can place the second view in the view hierarchy just before the animation starts (invisibly, because its alpha starts at 0) and remove the first view just after the animation ends (invisibly, because its alpha ends at 0):

let v2 = UIView()
v2.backgroundColor = .black
v2.alpha = 0
v2.frame = self.v.frame
self.v.superview!.addSubview(v2)
let anim = UIViewPropertyAnimator(duration: 1, curve: .linear) {
    self.v.alpha = 0
    v2.alpha = 1
}
anim.addCompletion { _ in
    self.v.removeFromSuperview()
}
anim.startAnimation()
Tip

Another way to remove a view from the view hierarchy with animation is to call the UIView class method perform(_:on:options:animations:completion:) with .delete as its first argument (this is, in fact, the only possible first argument). This causes the view to blur, shrink, and fade, and sends it removeFromSuperview() afterward.

Code that isn’t about animatable view properties can appear in an animations function with no problem, and will in fact run immediately when startAnimation is called. But we must be careful to keep any changes to animatable properties that we do not want animated out of the animations function. In the preceding example, in setting v2.alpha to 0, I just want to set it right now, instantly; I don’t want that change to be animated. So I’ve put that line outside the animations function (and in particular, before it).

Sometimes, though, that’s not so easy; perhaps, within the animations function, we must call a method that might perform unwanted animatable changes. The UIView class method performWithoutAnimation(_:) solves the problem; it goes inside an animations function, but whatever happens in its function is not animated. In this rather artificial example, the view jumps to its new position and then slowly turns red:

let anim = UIViewPropertyAnimator(duration: 1, curve: .linear) {
    self.v.backgroundColor = .red
    UIView.performWithoutAnimation {
        self.v.center.y += 100
    }
}
anim.startAnimation()

The material inside an animations function (but not inside a performWithoutAnimation function) orders the animation — that is, it gives instructions for what the animation will be when the redraw moment comes. If you change an animatable view property as part of the animation, you should not change that property again afterward; the results can be confusing, because there’s a conflict with the animation you’ve already ordered. This code, for example, is essentially incoherent:

let anim = UIViewPropertyAnimator(duration: 2, curve: .linear) {
    self.v.center.y += 100
}
self.v.center.y += 300
anim.startAnimation()

What actually happens is that the view jumps 300 points down and then animates 100 points further down. That’s probably not what you intended. After you’ve ordered an animatable view property to be animated inside an animations function, don’t change that view property’s value again until after the animation is over.

On the other hand, this code, while somewhat odd, nevertheless does a smooth single animation to a position 400 points further down:

let anim = UIViewPropertyAnimator(duration: 2, curve: .linear) {
    self.v.center.y += 100
    self.v.center.y += 300
}
anim.startAnimation()

That’s because basic positional view animations are additive by default. This means that the second animation is run simultaneously with the first, and is blended with it.

View Animation Configuration

The details of how you configure a view animation differ depending on whether you’re using a property animator or calling one of the UIView class methods. With a property animator, as my examples have already shown, you can configure the animator in several steps before telling it to start animating. With a UIView class method, on the other hand, everything has to be supplied in a single command, which both configures and orders the animation. The full form of the chief UIView class method is:

  • animate(withDuration:delay:options:animations:completion:)

There are shortened versions of the same command; for example, you can omit the delay: and options: parameters, and even the completion: parameter. But it’s still the same command, and the configuration of the animation is complete at this point.

Animations function

The animations function contains the commands setting animatable view properties. With a block-based UIView class method, this is the animations: parameter. With a property animator, the animations function is usually provided as the animations: argument when the property animator is instantiated. However, a property animator can have one or more animations functions added after instantiation, by calling addAnimations(_:), as we saw earlier.

Completion function

A completion function contains commands to be executed when the animation finishes. With a UIView class method, the completion function is the completion: parameter. It takes one parameter, a Bool reporting whether the animation finished.

A property animator can have multiple completion functions, provided by calling addCompletion(_:). The completion function takes one parameter, a UIViewAnimatingPosition reporting where the animation ended up: .end, .start, or .current. (I’ll talk later about what those values mean.) A property animator that is told to stop its animation with stopAnimation(_:) does not execute its completion functions until it is subsequently told to finish with finishAnimation(at:). The stopAnimation(_:) parameter comes into play here:

  • If you call stopAnimation(false) followed by finishAnimation(at:), the animator’s completion functions are then executed.

  • If you call stopAnimation(true), or if you call stopAnimation(false) but omit to call finishAnimation(at:), the animator’s completion functions are not executed.

Animation duration

The duration of an animation represents how long it takes (in seconds) to run from start to finish. You can also think of this as the animation’s speed. Obviously, if two views are told to move different distances in the same time, the one that must move further must move faster.

A duration of 0 doesn’t really mean 0. It means “use the default duration.” This fact will be of interest later when we talk about nesting animations. Outside of a nested animation, the default is two-tenths of a second.

With a block-based UIView class method, the animation duration is the duration: parameter. With a property animator, it is supplied as the duration: parameter when the property animator is initialized.

Animation delay

It is permitted to order the animation along with a delay before the animation goes into action. The default is no delay. A delay is not the same as applying the animation using delayed performance; the animation is applied immediately, but when it starts running it spins its wheels, with no visible change, until the delay time has elapsed.

With a block-based UIView class method, this is the delay: parameter. To apply a delay to an animation with a property animator, call startAnimation(afterDelay:) instead of startAnimation.

Animation timing

An animation has a timing curve that maps interpolated values to time. For example, the notion of moving a view downward by 100 points in the course of 1 second can have many meanings. Should we move at a constant rate the whole time? Should we move slowly at first and more quickly later? There are a lot of possibilities.

With a UIView class method, you get a choice of just four timing curves (supplied as part of the options: argument, as I’ll explain in a moment). But a property animator gives you very broad powers to configure the timing curve the way you want. This is such an important topic that I’ll deal with it in a separate section later.

Animation options

In a UIView class method, the options: argument is a bitmask combining additional options. Here are some of the chief options: values (UIView.AnimationOptions) that you might wish to use:

Timing curve

When supplied in this way, only four built-in timing curves are available. The term “ease” means that there is a gradual acceleration or deceleration between the animation’s central speed and the zero speed at its start or end. Specify one at most:

  • .curveEaseInOut (the default)

  • .curveEaseIn

  • .curveEaseOut

  • .curveLinear (constant speed throughout)

.repeat

If included, the animation will repeat indefinitely. There is no way, as part of this command, to specify a certain number of repetitions; you ask either to repeat forever or not at all. This feels like a serious oversight in the design of the block-based animation API; I’ll suggest a workaround in a moment.

.autoreverse

If included, the animation will run from start to finish (in the given duration time), and will then run from finish to start (also in the given duration time). The documentation’s claim that you can autoreverse only if you also repeat is incorrect; you can use either or both (or neither).

When using .autoreverse, you will want to clean up at the end so that the view is back in its original position when the animation is over. To see what I mean, consider this code:

let opts : UIView.AnimationOptions = .autoreverse
let xorig = self.v.center.x
UIView.animate(withDuration:1, delay: 0, options: opts, animations: {
    self.v.center.x += 100
    }, completion: nil
)

The view animates 100 points to the right and then animates 100 points back to its original position — and then jumps 100 points back to the right. The reason is that the last actual value we assigned to the view’s center x is 100 points to the right, so when the animation is over and the “animation movie” is whipped away, the view is revealed still sitting 100 points to the right. The solution is to move the view back to its original position in the completion: function:

let opts : UIView.AnimationOptions = .autoreverse
let xorig = self.v.center.x
UIView.animate(withDuration:1, delay: 0, options: opts, animations: {
    self.v.center.x += 100
    }, completion: { _ in
        self.v.center.x = xorig
})

Working around the inability to specify a finite number of repetitions is tricky. The simplest solution is to resort to a command from the earliest generation of animation methods:

let opts : UIView.AnimationOptions = .autoreverse
let xorig = self.v.center.x
UIView.animate(withDuration:1, delay: 0, options: opts, animations: {
    UIView.setAnimationRepeatCount(3) // *
    self.v.center.x += 100
    }, completion: { _ in
        self.v.center.x = xorig
})

There are also some options saying what should happen if we order an animation when another animation is already ordered or in-flight (so that we are effectively nesting animations):

.overrideInheritedDuration

Prevents inheriting the duration from a surrounding or in-flight animation (the default is to inherit it).

.overrideInheritedCurve

Prevents inheriting the timing curve from a surrounding or in-flight animation (the default is to inherit it).

.beginFromCurrentState

If this animation animates a property already being animated by an animation that is previously ordered or in-flight, then instead of canceling the previous animation (completing the requested change instantly), if that is what would normally happen, this animation will use the presentation layer to decide where to start, and, if possible, will “blend” its animation with the previous animation. There is usually little need for .beginFromCurrentState, because simple view animations are additive by default; however, I’ll demonstrate one possible use later in this chapter.

To illustrate what it means for animations to be additive, let’s take advantage of the fact that a property animator allows us to add a second animation that doesn’t take effect until some amount of the first animation has elapsed:

let anim = UIViewPropertyAnimator(duration: 2, curve: .easeInOut) {
    self.v.center.y += 100
}
anim.addAnimations({
    self.v.center.x += 100
    }, delayFactor: 0.5)
anim.startAnimation()

The delayFactor: of 0.5 means that the second animation will start halfway through the duration, which is 2 seconds. So the animated view heads straight downward for 1 second and then smoothly swoops off to the right while continuing down for another second, ending up 100 points down and 100 points to the right of where it started. The two animations might appear to conflict — they are both changing the center of our view, and they have different durations and therefore different speeds — but instead they blend together seamlessly.

An even stronger example is what happens when the two animations directly oppose one another:

let yorig = self.v.center.y
let anim = UIViewPropertyAnimator(duration: 2, curve: .easeInOut) {
    self.v.center.y += 100
}
anim.addAnimations({
    self.v.center.y = yorig
    }, delayFactor: 0.5)
anim.startAnimation()

That’s a smooth autoreversing animation. The animated view starts marching toward a point 100 points down from its original position, but at about the halfway point it smoothly — not abruptly or sharply — slows and reverses itself and returns to its original position.

Timing Curves

A timing curve maps the fraction of the animation’s time that has elapsed (the x-axis) against the fraction of the animation’s change that has occurred (the y-axis); its endpoints are therefore at (0.0,0.0) and (1.0,1.0), because at the beginning of the animation there has been no elapsed time and no change, and at the end of the animation all the time has elapsed and all the change has occurred. There are two kinds of timing curve: cubic Bézier curves and springing curves.

Cubic timing curves

A cubic Bézier curve is defined by its endpoints, where each endpoint needs only one Bézier control point to define the tangent to the curve. Because the curve’s endpoints are known, defining the two control points is sufficient to describe the entire curve. That is, in fact, how it is expressed.

So, for example, the built-in ease-in-out timing function is defined by the two control points (0.42,0.0) and (0.58,1.0) — that is, it’s a Bézier curve with one endpoint at (0.0,0.0), whose control point is (0.42,0.0), and the other endpoint at (1.0,1.0), whose control point is (0.58,1.0) (Figure 4-1).

pios 1701
Figure 4-1. An ease-in-out Bézier curve

With a UIView class method, you have a choice of four built-in timing curves; you specify one of them through the options: argument, as I’ve already explained.

With a property animator, you’ll specify a timing curve as part of initialization. That’s why I postponed telling you how to initialize a property animator until now. Here are three property animator initializers and how the timing curve is expressed when you call them:

init(duration:curve:animations:)

The curve: is a built-in timing curve, specified as a UIView.AnimationCurve enum. These are the same built-in timing curves as for a UIView class method:

  • .easeInOut

  • .easeIn

  • .easeOut

  • .linear

init(duration:controlPoint1:controlPoint2:animations:)

The timing curve is supplied as the two control points that define it.

init(duration:timingParameters:)

This is most general form of initializer; the other two are convenience initializers that call it. There’s no animations: parameter, so you’ll have to call addAnimations later to supply the animations function. The timingParameters: is an object adopting the UITimingCurveProvider protocol, which can be a UICubicTimingParameters instance or a UISpringTimingParameters instance (I’ll talk about springing timing curves in a moment). The UICubicTimingParameters initializers are:

init(animationCurve:)

The value is one of the four built-in timing curves that I already mentioned, specified as a UIView.AnimationCurve enum.

init()

Provides a fifth built-in timing curve, used as the default for many built-in behaviors.

init(controlPoint1:controlPoint2:)

Defines the timing curve by its control points.

For example, here’s a cubic timing curve that eases in very slowly and finishes up all in a rush, whipping quickly into place after about two-thirds of the time has elapsed. I call this the “clunk” timing function:

anim = UIViewPropertyAnimator(
    duration: 1, timingParameters:
        UICubicTimingParameters(
            controlPoint1:CGPoint(0.9,0.1),
            controlPoint2:CGPoint(0.7,0.9)))

Springing timing curves

A springing timing curve is the solution to a physics problem whose initial conditions describe a mass attached to a stretched spring. The animation mimics releasing the spring and letting it rush toward and settle down at the destination value.

Springing timing curves are much more useful and widespread than you might suppose. A springing animation doesn’t have to animate a view from place to place, and doesn’t have to look particularly springy to be effective. A small initial spring velocity and a high damping gives a normal animation that wouldn’t particularly remind anyone of a spring, but that does have a pleasingly rapid beginning and slow ending; many of Apple’s own system animations are actually spring animations of that type (consider, for example, the way folders open in the home screen).

To use a springing timing curve with UIView block-based animation, you call a different class method:

  • animate(withDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)

You’re supplying two parameters that vary the nature of the initial conditions, and hence the behavior of the animation over time:

Damping ratio

The damping: parameter is a number between 0.0 and 1.0 that describes the amount of final oscillation. A value of 1.0 is critically damped and settles directly into place; lower values are underdamped. A value of 0.8 just barely overshoots and snaps back to the final value. A value of 0.1 waggles around the final value for a while before settling down.

Initial velocity

Higher values cause greater overshoot, depending on the damping ratio. For example, with a damping ratio of 0.3, an initial velocity value of 1 overshoots a little and bounces about twice before settling into place, a value of 10 overshoots a bit further, and a value of 100 overshoots by more than twice the distance.

Normally, you’ll probably leave the initial velocity at zero. It is useful particularly when converting from a gesture to an animation — that is, where the user is moving a view and releases it, and you want a springing animation to take over from there, starting out at the same velocity that the user was applying at the moment of release.

With a property animator, once again, you’ll supply the timing curve as part of initialization:

init(duration:dampingRatio:animations:)

The dampingRatio: argument is the same as the damping: in the UIView class method I just described. The initial velocity is zero.

init(duration:timingParameters:)

This is the same initializer I discussed in connection with cubic timing curves. Recall that the timingParameters: is a UITimingCurveProvider; this can be a UISpringTimingParameters object, whose initializers are:

init(dampingRatio:)

You supply a damping ratio, and the initial velocity is zero.

init(dampingRatio:initialVelocity:)

The initialVelocity: is similar to the initialSpringVelocity: in the UIView class method I described a moment ago, except that it is a CGVector. Normally, only the x-component matters, in which case they are effectively the same thing; the y-component is considered only if what’s being animated follows a two-dimensional path — for example, if you’re changing both components of a view’s center.

init(mass:stiffness:damping:initialVelocity:)

A slightly different way of looking at the initial conditions. The overall duration: value is ignored; the actual duration will be calculated from the other parameters (and this calculated duration can be discovered by reading the resulting property animator’s duration). The first three parameters are in proportion to one another. A high mass: can cause a vast overshoot. A low stiffness: or a low damping: can result in a long settle-down time. Thus, the mass is usually quite small, while the stiffness and damping are usually quite large.

init()

The default spring animation; it is quite heavily damped, and settles into place in about half a second. The overall duration: value is ignored. In terms of the previous initializer, the mass: is 3, the stiffness: is 1000, the damping: is 500, and the initialVelocity: is (0,0).

Canceling a View Animation

Once a view animation is in-flight, how can you cancel it? And what should “cancel” mean in the first place? This is one of the key areas where a property animator shows off its special powers.

Canceling a block-based animation

To illustrate the problem, I’ll first show what you would have had to do before property animators were invented. Imagine a simple unidirectional positional animation, with a long duration so that we can interrupt it in midflight. To facilitate the explanation, I’ll conserve both the view’s original position and its final position in properties:

self.pOrig = self.v.center
self.pFinal = self.v.center
self.pFinal.x += 100
UIView.animateWithDuration(4, animations: {
    self.v.center = self.pFinal
})

Now imagine that we have a button that we can tap during that animation, and that this button is supposed to cancel the animation. How can we do that?

One possibility is to reach down to the CALayer level and call removeAllAnimations:

self.v.layer.removeAllAnimations()

That has the advantage of simplicity, but the effect is jarring: the “animation movie” is whipped away instantly, “jumping” the view to its final position, effectively doing what the system does automatically when the app goes into the background.

So let’s try to devise a more subtle form of cancellation: the view should hurry to its final position. This is a case where the additive nature of animations actually gets in our way. We cannot merely impose another animation that moves the view to its final position with a short duration, because this doesn’t cancel the existing animation. Therefore, we must remove the first animation manually. We already know how to do that: call removeAllAnimations. But we also know that if we do that, the view will jump to its final position; we want it to remain, for the moment, at its current position — meaning the animation’s current position. But where on earth is that?

To find out, we have to ask the view’s presentation layer where it currently is. We reposition the view at the location of its presentation layer, and then remove the animation, and then perform the final “hurry home” animation:

self.v.layer.position = self.v.layer.presentation()!.position
self.v.layer.removeAllAnimations()
UIView.animate(withDuration:0.1) {
    self.v.center = self.pFinal
}

Another alternative is that cancellation means hurrying the view back to its original position. In that case, animate the view’s center to its original position instead of its destination position:

self.v.layer.position = self.v.layer.presentation()!.position
self.v.layer.removeAllAnimations()
UIView.animate(withDuration:0.1) {
    self.v.center = self.pOrig
}

Yet another possibility is that cancellation means just stopping wherever we happen to be. In that case, omit the final animation:

self.v.layer.position = self.v.layer.presentation()!.position
self.v.layer.removeAllAnimations()

Canceling a property animator’s animation

Now I’ll show how do those things with a property animator. We don’t have to reach down to the level of the layer. We don’t call removeAllAnimations. We don’t query the presentation layer. We don’t have to memorize the start position or the end position. The property animator does all of that for us!

For the sake of ease and generality, let’s hold the animator in an instance property where all of our code can see it. Here’s how it is configured:

self.anim = UIViewPropertyAnimator(
    duration: 4, timingParameters: UICubicTimingParameters())
self.anim.addAnimations {
    self.v.center.x += 100
}
self.anim.startAnimation()

Here’s how to cancel the animation by hurrying home to its end:

self.anim.pauseAnimation()
self.anim.continueAnimation(withTimingParameters: nil, durationFactor: 0.1)

We first pause the animation, because otherwise we can’t make changes to it. But the animation does not visibly pause, because we resume at once with a modification of the original animation, which is smoothly blended into the existing animation. The short durationFactor: is the “hurry” part; we want a much shorter duration than our animation’s original duration. We don’t have to tell the animator where to animate to; in the absence of any other commands, it animates to its original destination. The nil value for the timingParameters: tells the animation to use the existing timing curve.

What about canceling the animation by hurrying home to its beginning? It’s exactly the same, except that we reverse the animation:

self.anim.pauseAnimation()
self.anim.isReversed = true
self.anim.continueAnimation(withTimingParameters: nil, durationFactor: 0.1)

Again, we don’t have to tell the animator where to animate to; it knows where we started, and reversing means to go there.

Using the same technique, we could interrupt the animation and hurry to anywhere we like — by adding another animations function before continuing. Here, for example, cancellation causes us to rush right off the screen:

self.anim.pauseAnimation()
self.anim.addAnimations {
    self.v.center = CGPoint(-200,-200)
}
self.anim.continueAnimation(withTimingParameters: nil, durationFactor: 0.1)

What about canceling the animation by stopping wherever we are? Just stop the animation:

self.anim.stopAnimation(false)
self.anim.finishAnimation(at: .current)

Recall that the false argument means: “Please allow me to call finishAnimation(at:).” We want to call finishAnimation(at:) in order to specify where the view should end up when the “animation movie” is removed. By passing in .current, we state that we want the animated view to end up right where it is now. If we were to pass in .start or .end, the view would jump to that position (if it weren’t there already).

We can now understand the incoming parameter in the completion function! It is the position where we ended up. If the animation finished by proceeding to its end, that parameter is .end. If we reversed the animation and it finished by proceeding back to its start, as in our second cancellation example, that parameter is .start. If we called finishAnimation(at:), it is the at: argument we specified in the call.

Canceling a repeating animation

Finally, suppose that the animation we want to cancel is an infinitely repeating autoreversing animation. It will presumably be created with the UIView class method:

self.pOrig = self.v.center
let opts : UIView.AnimationOptions = [.autoreverse, .repeat]
UIView.animate(withDuration:1, delay: 0, options: opts, animations: {
    self.v.center.x += 100
})

Let’s say our idea of cancellation is to have the animated view hurry back to its original position; that is why we have saved the original position as an instance property. This is a situation where the .beginFromCurrentState option is useful! That’s because a repeating animation is not additive with a further animation. It is therefore sufficient simply to impose the “hurry” animation on top of the existing repeating animation, because it contradicts the repeating animation and therefore also cancels it. The .beginFromCurrentState option prevents the view from jumping momentarily to the “final” position, 100 points to the right, to which we set it when we initiated the repeating animation:

let opts : UIView.AnimationOptions = .beginFromCurrentState
UIView.animate(withDuration:0.1, delay:0, options:opts, animations: {
    self.v.center = self.pOrig
})

(In that example, I’m storing the view’s original position as a view controller property. If you find that objectionable because it’s not a very encapsulated approach, then consider storing it instead in the view’s layer, using key–value coding. The implementation is left as an exercise for the reader.)

Frozen View Animation

Another important feature of a property animator is that its animation can be frozen. We already know that the animation can be paused — or never even started. A frozen animation is simply left in this state. It can be started or resumed at any time subsequently; or we can keep the animation frozen, but move it to a different “frame” of the animation by setting its fractionComplete, thus controlling the frozen animation manually.

In this simple example, we have in the interface a slider (a UISlider) and a small red square view. As the user slides the slider from left to right, the red view follows along — and gradually turns green, depending how far the user slides the slider. If the user slides the slider all the way to the right, the view is at the right and is fully green. If the user slides the slider all the way back to the left, the view is at the left and is fully red.

To accomplish this, the property animator is configured with an animation moving the view all the way to right and turning it all the way green. But the animation is never started:

self.anim = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
    self.v.center.x = self.pTarget.x
    self.v.backgroundColor = .green()
}

The slider, whenever the user moves it, simply changes the animator’s fractionComplete to match its own percentage:

self.anim.fractionComplete = CGFloat(slider.value)

Apple refers to this technique of manually moving a frozen animation back and forth from frame to frame as scrubbing. A common use case is that the user will touch and move the animated view itself. This will come in handy in connection with interactive view controller transitions in Chapter 6.

In that example, I deliberately set the timing curve to .easeInOut in order to illustrate the real purpose of the scrubsLinearly property. You would think that a nonlinear timing curve would affect the relationship between the position of the slider and the position of the view: with an .easeInOut timing curve, for example, the view would arrive at the far right before the slider does. But that doesn’t happen, because a nonrunning animation switches its timing curve to .linear automatically for as long as it is nonrunning. The purpose of the scrubsLinearly property, whose default property is true, is to allow you to turn off that behavior by setting it to false on the rare occasions when this might be desirable.

Custom Animatable View Properties

You can define your own custom view property that can be animated by changing it in an animations function, provided the custom view property itself changes an animatable view property.

For example, imagine a UIView subclass, MyView, which has a Bool swing property. All this does is reposition the view: when swing is set to true, the view’s center x-coordinate is increased by 100; when swing is set to false, it is decreased by 100. A view’s center is animatable, so the swing property itself can be animatable.

The trick (suggested by an Apple WWDC 2014 video) is to implement MyView’s swing setter with a zero-duration animation:

class MyView : UIView {
    var swing : Bool = false {
        didSet {
            var p = self.center
            p.x = self.swing ? p.x + 100 : p.x - 100
            UIView.animate(withDuration:0) {
                self.center = p
            }
        }
    }
}

If we now change a MyView’s swing directly, the view jumps to its new position; there is no animation. But if an animations function changes the swing property, the swing setter’s animation inherits the duration of the surrounding animations function — because such inheritance is, as I mentioned earlier, the default. Thus the change in position is animated, with the specified duration:

let anim = UIViewPropertyAnimator(duration: 1, curve: .easeInOut) {
    self.v.swing.toggle()
}
anim.startAnimation()

Keyframe View Animation

A view animation can be described as a set of keyframes. This means that, instead of a simple beginning and end point, you specify multiple stages in the animation and those stages are joined together for you. This can be useful as a way of chaining animations together, or as a way of defining a complex animation that can’t be described as a single change of value.

To create a keyframe animation, you call this UIView class method:

  • animateKeyframes(withDuration:delay:options:animations:completion:)

It takes an animations function, and inside that function you call this UIView class method multiple times to specify each stage:

  • addKeyframe(withRelativeStartTime:relativeDuration:animations:)

Each keyframe’s start time and duration is between 0 and 1, relative to the animation as a whole. (Giving a keyframe’s start time and duration in seconds is a common beginner mistake.)

For example, here I’ll waggle a view back and forth horizontally while moving it down the screen vertically:

var p = self.v.center
let dur = 0.25
var start = 0.0
let dx : CGFloat = 100
let dy : CGFloat = 50
var dir : CGFloat = 1
UIView.animateKeyframes(withDuration:4, delay: 0, animations: {
    UIView.addKeyframe(withRelativeStartTime:start,
        relativeDuration: dur) {
            p.x += dx*dir; p.y += dy
            self.v.center = p
        }
    start += dur; dir *= -1
    UIView.addKeyframe(withRelativeStartTime:start,
        relativeDuration: dur) {
            p.x += dx*dir; p.y += dy
            self.v.center = p
        }
    start += dur; dir *= -1
    UIView.addKeyframe(withRelativeStartTime:start,
        relativeDuration: dur) {
            p.x += dx*dir; p.y += dy
            self.v.center = p
        }
    start += dur; dir *= -1
    UIView.addKeyframe(withRelativeStartTime:start,
        relativeDuration: dur) {
            p.x += dx*dir; p.y += dy
            self.v.center = p
        }
})

In that code, there are four keyframes, evenly spaced: each is 0.25 in duration (one-fourth of the whole animation) and each starts 0.25 later than the previous one (as soon as the previous one ends). In each keyframe, the view’s center x-coordinate increases or decreases by 100, alternately, while its center y-coordinate keeps increasing by 50.

The keyframe values are points in space and time; the actual animation interpolates between them. How this interpolation is done depends upon the options: parameter (omitted in the preceding code). Several UIView.KeyframeAnimationOptions values have names that start with calculationMode; pick one. The default is .calculationModeLinear. In our example, this means that the path followed by the view is a sharp zig-zag, the view seeming to bounce off invisible walls at the right and left. But if our choice is .calculationModeCubic, our view describes a smooth S-curve, starting at the view’s initial position and ending at the last keyframe point, and passing through the three other keyframe points like the maxima and minima of a sine wave.

Because my keyframes are perfectly even, I could achieve the same effects by using .calculationModePaced or .calculationModeCubicPaced, respectively. The paced options ignore the relative start time and relative duration values of the keyframes; you might as well pass 0 for all of them. Instead, they divide up the times and durations evenly, exactly as my code has done.

Finally, .calculationModeDiscrete means that the changed animatable properties don’t animate: the animation jumps to each keyframe.

The outer animations function can contain other changes to animatable view properties, as long as they don’t conflict with the addKeyframe animations; these are animated over the total duration. For example:

UIView.animateKeyframes(withDuration:4, delay: 0, animations: {
    self.v.alpha = 0
    // ...

The result is that as the view zigzags back and forth down the screen, it also gradually fades away.

It is also legal and meaningful to supply a timing curve as part of the options: argument. Unfortunately, the documentation fails to make this clear; and Swift’s obsessive-compulsive attitude toward data types resists folding a UIView.AnimationOptions timing curve directly into a value typed as a UIView.KeyframeAnimationOptions. Yet if you don’t do it, the default is .curveEaseInOut, which may not be what you want. Here’s how to combine .calculationModeLinear with .curveLinear:

var opts : UIView.KeyframeAnimationOptions = .calculationModeLinear
let opt2 : UIView.AnimationOptions = .curveLinear
opts.insert(UIView.KeyframeAnimationOptions(rawValue:opt2.rawValue))

That’s two different senses of linear! The first means that the path described by the moving view is a sequence of straight lines. The second means that the moving view’s speed along that path is steady.

You might want to pause or reverse a keyframe view animation by way of a property animator. To do so, nest your call to UIView.animateKeyframes... inside the property animator’s animations function. The property animator’s duration and timing curve are then inherited, so this is another way to dictate the keyframe animation’s timing.

The power of keyframe animations often goes unappreciated by beginners. Keyframes do not have to be sequential, nor do they all have to involve the same property. Thus, they can be used to coordinate different animations. In this example, our view animates slowly to the right, and changes color suddenly in the middle of its movement:

let anim = UIViewPropertyAnimator(
    duration: 4, timingParameters: UICubicTimingParameters())
anim.addAnimations {
    UIView.animateKeyframes(withDuration: 0, delay: 0, animations: {
        UIView.addKeyframe(withRelativeStartTime: 0,
            relativeDuration: 1) {
                self.v.center.x += 100
        }
        UIView.addKeyframe(withRelativeStartTime: 0.5,
            relativeDuration: 0.25) {
                self.v.backgroundColor = .red
        }
    })
}
anim.startAnimation()

There are other ways to arrange the same outward effect, to be sure; but this way, the entire animation is placed under the control of a single property animator, and is thus easy to pause, scrub, reverse, and so on.

Transitions

A transition is an animation that emphasizes a view’s change of content. Transitions are ordered using one of two UIView class methods:

  • transition(with:duration:options:animations:completion:)

  • transition(from:to:duration:options:completion:)

The transition animation types are expressed as part of the options: bitmask:

  • .transitionFlipFromLeft, .transitionFlipFromRight

  • .transitionCurlUp, .transitionCurlDown

  • .transitionFlipFromBottom, .transitionFlipFromTop

  • .transitionCrossDissolve

Transitioning one view

transition(with:...) takes one UIView parameter, and performs the transition animation on that view. In this example, a UIImageView containing an image of Mars flips over as its image changes to a smiley face; it looks as if the image view were two-sided, with Mars on one side and the smiley face on the other:

let opts : UIView.AnimationOptions = .transitionFlipFromLeft
UIView.transition(with:self.iv, duration: 0.8, options: opts, animations: {
    self.iv.image = UIImage(named:"Smiley")
})

In that example, I’ve put the content change inside the animations function. That’s conventional but misleading; the truth is that if all that’s changing is the content, nothing needs to go into the animations function. The change of content can be anywhere, before or even after this entire line of code. It’s the flip that’s being animated. You might use the animations function here to order additional animations, such as a change in a view’s center.

You can do the same sort of thing with a custom view that does its own drawing. Let’s say that I have a UIView subclass, MyView, that draws either a rectangle or an ellipse depending on the value of its Bool reverse property:

class MyView : UIView {
    var reverse = false
    override func draw(_ rect: CGRect) {
        let f = self.bounds.insetBy(dx: 10, dy: 10)
        let con = UIGraphicsGetCurrentContext()!
        if self.reverse {
            con.strokeEllipse(in:f)
        }
        else {
            con.stroke(f)
        }
    }
}

This code flips a MyView instance while changing its drawing from a rectangle to an ellipse or vice versa:

let opts : UIView.AnimationOptions = .transitionFlipFromLeft
self.v.reverse.toggle()
UIView.transition(with:self.v, duration: 1, options: opts, animations: {
    self.v.setNeedsDisplay()
})

By default, if a view has subviews whose layout changes as part of a transition animation, that change in layout is not animated: the layout changes directly to its final appearance when the transition ends. If you want to display a subview of the transitioning view being animated as it assumes its final state, include .allowAnimatedContent in the options: bitmask.

Transitioning two views and their superview

transition(from:to:...) takes two UIView parameters; the first view is replaced by the second, while their superview undergoes the transition animation. There are two possible configurations, depending on the options: you provide:

Remove one subview, add the other

If .showHideTransitionViews is not one of the options:, then the second subview is not in the view hierarchy when we start; the transition removes the first subview from its superview and adds the second subview to that same superview.

Hide one subview, show the other

If .showHideTransitionViews is one of the options:, then both subviews are in the view hierarchy when we start; the isHidden of the first is false, the isHidden of the second is true, and the transition reverses those values.

In this example, a label self.lab is already in the interface. The animation causes the superview of self.lab to flip over, while at the same time a different label, lab2, is substituted for the existing label:

let lab2 = UILabel(frame:self.lab.frame)
lab2.text = self.lab.text == "Hello" ? "Howdy" : "Hello"
lab2.sizeToFit()
UIView.transition(from:self.lab, to: lab2,
    duration: 0.8, options: .transitionFlipFromLeft) { _ in
        self.lab = lab2
}

It’s up to you to make sure beforehand that the second view has the desired position, so that it will appear in the right place in its superview.

Transitions are an underutilized iOS animation feature. Earlier, for example, I demonstrated how to replace one view with another by adding the second view, animating the alpha values of both views, and removing the first view in the completion function. That’s a common technique, but calling transition(from:to:...) with a .transitionCrossDissolve animation is simpler and does the same thing.

Implicit Layer Animation

Animating a layer can be as simple as setting a property. A change in what the documentation calls an animatable property is automatically interpreted as a request to animate that change. In other words, animation of layer property changes is the default! Multiple property changes are considered part of the same animation. This mechanism is called implicit animation.

You may be wondering: if implicit animation is the default, why didn’t we notice it happening in any of the layer examples in Chapter 3? It’s because there are two common situations where implicit layer animation doesn’t happen:

  • Implicit layer animation doesn’t operate on a UIView’s underlying layer. You can animate a UIView’s underlying layer directly, but you must use explicit layer animation (discussed later in this chapter).

  • Implicit layer animation doesn’t affect a layer as it is being created, configured, and added to the interface. Implicit animation comes into play when you change an animatable property of a layer that is already present in the interface.

In Chapter 3 we constructed a compass out of layers. The compass itself is a CompassView that does no drawing of its own; its underlying layer is a CompassLayer that also does no drawing, serving only as a superlayer for the layers that constitute the drawing. None of the layers that constitute the actual drawing is the underlying layer of a view, so a property change to any of them, once they are established in the interface, is animated automatically.

So, presume that we have established all our compass layers in the interface. And suppose we have a reference to the arrow layer (arrow). If we rotate the arrow layer simply by changing its transform property, the arrow rotation is animated:

arrow.transform = CATransform3DRotate(arrow.transform, .pi/4.0, 0, 0, 1)

CALayer properties listed in the documentation as animatable in this way are anchorPoint and anchorPointZ, backgroundColor, borderColor, borderWidth, bounds, contents, contentsCenter, contentsRect, cornerRadius, isDoubleSided, isHidden, masksToBounds, opacity, position and zPosition, rasterizationScale and shouldRasterize, shadowColor, shadowOffset, shadowOpacity, shadowRadius, and sublayerTransform and transform.

In addition, a CAShapeLayer’s path, strokeStart, strokeEnd, fillColor, strokeColor, lineWidth, lineDashPhase, and miterLimit are animatable; so are a CATextLayer’s fontSize and foregroundColor, and a CAGradientLayer’s colors, locations, and endPoint.

Basically, a property is animatable because there’s some sensible way to interpolate the intermediate values between one value and another. The nature of the animation attached to each property is therefore generally just what you would intuitively expect. When you change a layer’s isHidden property, it fades out of view (or into view). When you change a layer’s contents, the old contents are dissolved into the new contents. And so forth.

Warning

A layer’s cornerRadius is animatable by explicit layer animation, or by view animation, but not by implicit layer animation.

Animation Transactions

Animation operates with respect to a transaction (a CATransaction), which collects all animation requests and hands them over to the animation server in a single batch. Every animation request takes place in the context of some transaction. You can make this explicit by wrapping your animation requests in calls to the CATransaction class methods begin and commit; the result is a transaction block. Additionally, there is always an implicit transaction surrounding your code, and you can operate on this implicit transaction without any begin and commit.

To modify the characteristics of an implicit animation, you modify the transaction that surrounds it. Typically, you’ll use these CATransaction class methods:

setAnimationDuration(_:)

The duration of the animation.

setAnimationTimingFunction(_:)

A CAMediaTimingFunction; layer timing functions are discussed in the next section.

setDisableActions(_:)

Toggles implicit animations for this transaction.

setCompletionBlock(_:)

A function (taking no parameters) to be called when the animation ends; it is called even if no animation is triggered during this transaction.

CATransaction also implements key–value coding to allow you to set and retrieve a value for an arbitrary key, similar to CALayer.

By nesting transaction blocks, you can apply different animation characteristics to different elements of an animation. You can also use transaction commands outside of any transaction block to modify the implicit transaction. So, in our previous example, we could slow down the animation of the arrow like this:

CATransaction.setAnimationDuration(0.8)
arrow.transform = CATransform3DRotate(arrow.transform, .pi/4.0, 0, 0, 1)

An important use of transactions is to turn implicit animation off. This is valuable because implicit animation is the default, and can be unwanted (and a performance drag). To turn off implicit animation, call setDisableActions(true). There are other ways to turn off implicit animation (discussed later in this chapter), but this is the simplest.

setCompletionBlock(_:) establishes a completion function that signals the end, not only of the implicit layer property animations you yourself have ordered as part of this transaction, but of all animations ordered during this transaction, including Cocoa’s own animations. Thus, it’s a way to be notified when any and all animations come to an end.

The “redraw moment” that I’ve spoken of in connection with drawing, layout, layer property settings, and animation is actually the end of the current transaction. Thus, for example:

  • You set a view’s background color; the displayed color of the background is changed when the transaction ends.

  • You call setNeedsDisplay; draw(_:) is called when the transaction ends.

  • You call setNeedsLayout; layout happens when the transaction ends.

  • You order an animation; the animation starts when the transaction ends.

What’s really happening is this. Your code runs within an implicit transaction. Your code comes to an end, and the transaction commits itself. It is then, as part of the transaction commit procedure, that the screen is updated: first layout, then drawing, then obedience to layer property changes, then the start of any animations. The animation server then continues operating on a background thread; it has kept a reference to the transaction, and calls its completion function, if any, when the animations are over.

Warning

An explicit transaction block that orders an animation to a layer, if the block is not preceded by any other changes to the layer, can cause animation to begin immediately when the CATransaction class method commit is called, without waiting for the redraw moment, while your code continues running. In my experience, this can cause trouble (animation delegate messages cannot arrive, and the presentation layer can’t be queried properly) and should be avoided.

Media Timing Functions

The CATransaction class method setAnimationTimingFunction(_:) takes as its parameter a media timing function (CAMediaTimingFunction). This is the Core Animation way of describing the same cubic Bézier timing curves I discussed earlier.

To specify a built-in timing curve, call the CAMediaTimingFunction initializer init(name:) with one of these parameters (CAMediaTimingFunctionName):

  • .linear

  • .easeIn

  • .easeOut

  • .easeInEaseOut

  • .default

To define your own timing curve, supply the coordinates of the two Bézier control points by calling init(controlPoints:). Here we define the “clunk” timing curve and apply it to the rotation of the compass arrow:

let clunk = CAMediaTimingFunction(controlPoints: 0.9, 0.1, 0.7, 0.9)
CATransaction.setAnimationTimingFunction(clunk)
arrow.transform = CATransform3DRotate(arrow.transform, .pi/4.0, 0, 0, 1)

Core Animation

Core Animation is the fundamental underlying iOS animation technology. View animation and implicit layer animation are merely convenient façades for Core Animation. Core Animation is explicit layer animation.

When you want to animate a layer, especially a layer that is not a view’s underlying layer, Core Animation is vastly more powerful than simple implicit layer animation. Moreover, Core Animation works on a view’s underlying layer; it is the only way to apply full-on layer property animation to a view. Thus only Core Animation allows you to transcend the limited repertoire of animatable view properties. (On the other hand, animating a view’s underlying layer with Core Animation is layer animation, not view animation — so you don’t get any automatic layout of that view’s subviews. This can be a reason for preferring view animation.)

CABasicAnimation and Its Inheritance

The simplest way to animate a property with Core Animation is with a CABasicAnimation object. CABasicAnimation derives much of its power through its inheritance, so I’ll describe that inheritance along with CABasicAnimation itself. You will readily see that all the property animation features we have met already are embodied in a CABasicAnimation instance.

CAAnimation

CAAnimation is an abstract class, meaning that you’ll only ever use a subclass of it. Some of CAAnimation’s powers come from its implementation of the CAMediaTiming protocol.

delegate

An adopter of the CAAnimationDelegate protocol. The delegate messages are:

  • animationDidStart(_:)

  • animationDidStop(_:finished:)

A CAAnimation instance retains its delegate; this is very unusual behavior and can cause trouble if you’re not conscious of it (I’m speaking from experience). Alternatively, don’t set a delegate; to make your code run after the animation ends, call the CATransaction class method setCompletionBlock(_:) before configuring the animation.

duration, timingFunction

The length of the animation, and its timing function (a CAMediaTimingFunction). A duration of 0 (the default) means 0.25 seconds unless overridden by the transaction.

autoreverses, repeatCount, repeatDuration

For an infinite repeatCount, use Float.greatestFiniteMagnitude. The repeatDuration property is a different way to govern repetition, specifying how long the repetition should continue rather than how many repetitions should occur; don’t specify both a repeatCount and a repeatDuration.

beginTime

The delay before the animation starts. To delay an animation with respect to now, call CACurrentMediaTime and add the desired delay in seconds. The delay does not eat into the animation’s duration.

timeOffset

A shift in the animation’s overall timing; looked at another way, specifies the starting frame of the “animation movie,” which is treated as a loop. For example, consider an animation with a duration of 8 and a time offset of 4: it plays its second half followed by its first half.

CAAnimation, along with all its subclasses, implements key–value coding to allow you to set and retrieve a value for an arbitrary key, similar to CALayer (Chapter 3) and CATransaction.

CAPropertyAnimation

CAPropertyAnimation is a subclass of CAAnimation. It too is abstract, and adds the following:

keyPath

The all-important string specifying the CALayer key that is to be animated. Recall from Chapter 3 that CALayer properties are accessible through KVC keys; now we are using those keys! The convenience initializer init(keyPath:) creates the instance and assigns it a keyPath.

isAdditive

If true, the values supplied by the animation are added to the current presentation layer value.

isCumulative

If true, a repeating animation starts each repetition where the previous repetition ended rather than jumping back to the start value.

valueFunction

Converts a simple scalar value that you supply into a transform.

Warning

There is no animatable CALayer key called "frame". To animate a layer’s frame using explicit layer animation, if both its position and bounds are to change, you must animate both. Similarly, you cannot use explicit layer animation to animate a layer’s affineTransform property, because affineTransform is not a property (it’s a pair of convenience methods); you must animate its transform instead. Attempting to form an animation with a key path of "frame" or "affineTransform" is a common beginner error.

CABasicAnimation

CABasicAnimation is a subclass (not abstract!) of CAPropertyAnimation. It adds the following:

fromValue, toValue

The starting and ending values for the animation. These values must be Objective-C objects, so numbers and structs will have to be wrapped accordingly, using NSNumber and NSValue; fortunately, Swift will automatically take care of this for you. If neither fromValue nor toValue is provided, the former and current values of the property are used. If just one of them is provided, the other uses the current value of the property.

byValue

Expresses one of the endpoint values as a difference from the other rather than in absolute terms. So you would supply a byValue instead of a fromValue or instead of a toValue, and the actual fromValue or toValue would be calculated for you by subtraction or addition with respect to the other value. If you supply only a byValue, the fromValue is the property’s current value.

Using a CABasicAnimation

Having constructed and configured a CABasicAnimation, the way you order it to be performed is to add it to a layer. This is done with the CALayer instance method add(_:forKey:). (I’ll discuss the purpose of the forKey: parameter later; it’s fine to ignore it and use nil, as I do in the examples that follow.)

However, there’s a slight twist. A CAAnimation is merely an animation; all it does is describe the hoops that the presentation layer is to jump through, the “animation movie” that is to be presented. It has no effect on the layer itself. Thus, if you naïvely create a CABasicAnimation and add it to a layer with add(_:forKey:), the animation happens and then the “animation movie” is whipped away to reveal the layer sitting there in exactly the same state as before. It is up to you to change the layer to match what the animation will ultimately portray. The converse, of course, is that you don’t have to change the layer if it doesn’t change as a result of the animation.

To ensure good results, start by taking a plodding, formulaic approach to the use of CABasicAnimation, like this:

  1. Capture the start and end values for the layer property you’re going to change, because you’re likely to need these values in what follows.

  2. Change the layer property to its end value, first calling setDisableActions(true) if necessary to prevent implicit animation.

  3. Construct the explicit animation, using the start and end values you captured earlier, and with its keyPath corresponding to the layer property you just changed.

  4. Add the explicit animation to the layer.

An explicit animation is copied when it is added to a layer. That’s why the animation must be configured first and added to the layer later. The copy added to the layer is immutable after that.

Here’s how you’d use this approach to animate our compass arrow rotation:

// capture the start and end values
let startValue = arrow.transform
let endValue = CATransform3DRotate(startValue, .pi/4.0, 0, 0, 1)
// change the layer, without implicit animation
CATransaction.setDisableActions(true)
arrow.transform = endValue
// construct the explicit animation
let anim = CABasicAnimation(keyPath:#keyPath(CALayer.transform))
anim.duration = 0.8
let clunk = CAMediaTimingFunction(controlPoints:0.9, 0.1, 0.7, 0.9)
anim.timingFunction = clunk
anim.fromValue = startValue
anim.toValue = endValue
// ask for the explicit animation
arrow.add(anim, forKey:nil)

Once you’re comfortable with the full form, you will find that in many cases it can be condensed. For example, when the fromValue and toValue are not set, the former and current values of the property are used automatically. (This magic is possible because, at the time the CABasicAnimation is added to the layer, the presentation layer still has the former value of the property, while the layer itself has the new value; thus, the CABasicAnimation is able to retrieve them.) In our example, therefore, there is no need to set the fromValue and toValue, and no need to capture the start and end values beforehand. Here’s the condensed version:

CATransaction.setDisableActions(true)
arrow.transform = CATransform3DRotate(arrow.transform, .pi/4.0, 0, 0, 1)
let anim = CABasicAnimation(keyPath:#keyPath(CALayer.transform))
anim.duration = 0.8
let clunk = CAMediaTimingFunction(controlPoints:0.9, 0.1, 0.7, 0.9)
anim.timingFunction = clunk
arrow.add(anim, forKey:nil)

As I mentioned earlier, you will omit changing the layer if it doesn’t change as a result of the animation. For example, let’s make the compass arrow appear to vibrate rapidly, without ultimately changing its current orientation. To do this, we’ll waggle it back and forth, using a repeated animation, between slightly clockwise from its current position and slightly counterclockwise from its current position. The “animation movie” neither starts nor stops at the current position of the arrow, but for this animation it doesn’t matter, because it all happens so quickly as to appear natural:

// capture the start and end values
let nowValue = arrow.transform
let startValue = CATransform3DRotate(nowValue, .pi/40.0, 0, 0, 1)
let endValue = CATransform3DRotate(nowValue, -.pi/40.0, 0, 0, 1)
// construct the explicit animation
let anim = CABasicAnimation(keyPath:#keyPath(CALayer.transform))
anim.duration = 0.05
anim.timingFunction = CAMediaTimingFunction(name:.linear)
anim.repeatCount = 3
anim.autoreverses = true
anim.fromValue = startValue
anim.toValue = endValue
// ask for the explicit animation
arrow.add(anim, forKey:nil)

That code, too, can be shortened considerably from its full form. We can eliminate the need to calculate the new rotation values based on the arrow’s current transform by setting our animation’s isAdditive property to true; this means that the animation’s property values are added to the existing property value for us, so that they are relative, not absolute. For a transform, “added” means “matrix-multiplied,” so we can describe the waggle without any reference to the arrow’s current rotation. Moreover, because our rotation is so simple (around a cardinal axis), we can take advantage of CAPropertyAnimation’s valueFunction; the animation’s property values can then be simple scalars (in this case, angles), because the valueFunction tells the animation to interpret these as rotations around the z-axis:

let anim = CABasicAnimation(keyPath:#keyPath(CALayer.transform))
anim.duration = 0.05
anim.timingFunction = CAMediaTimingFunction(name:.linear)
anim.repeatCount = 3
anim.autoreverses = true
anim.isAdditive = true
anim.valueFunction = CAValueFunction(name:.rotateZ)
anim.fromValue = Float.pi/40
anim.toValue = -Float.pi/40
arrow.add(anim, forKey:nil)
Warning

Instead of using a valueFunction, we could have set the animation’s key path to "transform.rotation.z" to achieve the same effect. However, Apple advises against this, as it can result in mathematical trouble when there is more than one rotation.

Let’s return once more to our arrow “clunk” rotation for another implementation, this time using the isAdditive and valueFunction properties. We set the arrow layer to its final transform at the outset, so when the time comes to configure the animation, its toValue, in isAdditive terms, will be 0; the fromValue will be its current value expressed negatively, like this:

let rot = CGFloat.pi/4.0
CATransaction.setDisableActions(true)
arrow.transform = CATransform3DRotate(arrow.transform, rot, 0, 0, 1)
// construct animation additively
let anim = CABasicAnimation(keyPath:#keyPath(CALayer.transform))
anim.duration = 0.8
let clunk = CAMediaTimingFunction(controlPoints:0.9, 0.1, 0.7, 0.9)
anim.timingFunction = clunk
anim.fromValue = -rot
anim.toValue = 0
anim.isAdditive = true
anim.valueFunction = CAValueFunction(name:.rotateZ)
arrow.add(anim, forKey:nil)

That is an interesting way of describing the animation; in effect, it expresses the animation in reverse, regarding the final position as correct and the current position as an aberration to be corrected. It also happens to be how additive view animations are rewritten behind the scenes, and explains their behavior.

Tip

Interesting effects can be achieved by using explicit layer animation, such as a CABasicAnimation, on a CAReplicatorLayer. I’ll give an example in Chapter 12.

Springing Animation

Springing animation is exposed at the Core Animation level through the CASpringAnimation class (a CABasicAnimation subclass). Its properties are the same as the parameters of the fullest form of the UISpringTimingParameters initializer, except that its initialVelocity is a CGFloat, not a CGVector. The duration is ignored, but don’t omit it. The actual duration calculated from your specifications can be extracted as the settlingDuration property. For example:

CATransaction.setDisableActions(true)
self.v.layer.position.y += 100
let anim = CASpringAnimation(keyPath: #keyPath(CALayer.position))
anim.damping = 0.7
anim.initialVelocity = 20
anim.mass = 0.04
anim.stiffness = 4
anim.duration = 1 // ignored, but you need to supply something
self.v.layer.add(anim, forKey: nil)

Keyframe Animation

Keyframe animation (CAKeyframeAnimation) is an alternative to basic animation (CABasicAnimation); they are both subclasses of CAPropertyAnimation, and they are used in similar ways. The difference is that you need to tell the keyframe animation what the keyframes are. In the simplest case, you can just set its values array. This tells the animation its starting value, its ending value, and some specific values through which it should pass on the way between them.

Here’s a new version of our animation for waggling the compass arrow, expressing it as a keyframe animation. The stages include the start and end states along with eight alternating waggles in between, with the degree of waggle becoming progressively smaller:

var values = [0.0]
let directions = sequence(first:1) {$0 * -1}
let bases = stride(from: 20, to: 60, by: 5)
for (base, dir) in zip(bases, directions) {
    values.append(Double(dir) * .pi / Double(base))
}
values.append(0.0)
let anim = CAKeyframeAnimation(keyPath:#keyPath(CALayer.transform))
anim.values = values
anim.isAdditive = true
anim.valueFunction = CAValueFunction(name: .rotateZ)
arrow.add(anim, forKey:nil)

Here are some CAKeyframeAnimation properties:

values

The array of values that the animation is to adopt, including the starting and ending value.

timingFunctions

An array of timing functions, one for each stage of the animation (so that this array will be one element shorter than the values array).

keyTimes

An array of times to accompany the array of values, defining when each value should be reached. The times start at 0 and are expressed as increasing fractions of 1, ending at 1.

calculationMode

Describes how the values are treated to create all the values through which the animation must pass (CAAnimationCalculationMode):

.linear

The default. A simple straight-line interpolation from value to value.

.cubic

Constructs a single smooth curve passing through all the values (and additional advanced properties, tensionValues, continuityValues, and biasValues, allow you to refine the curve).

.paced, .cubicPaced

The timing functions and key times are ignored, and the velocity is made constant through the whole animation.

.discrete

No interpolation: we jump directly to each value at the corresponding key time.

path

When you’re animating a property whose values are pairs of floats (CGPoints), this is an alternative way of describing the values; instead of a values array, which must be interpolated to arrive at the intermediate values along the way, you supply the entire interpolation as a single CGPath. The points used to define the path are the keyframe values, so you can still apply timing functions and key times. If you’re animating a position, the rotationMode property lets you ask the animated object to rotate so as to remain perpendicular to the path.

In this example, the values array is a sequence of five images (self.images) to be presented successively and repeatedly in a layer’s contents, like the frames in a movie; the effect is similar to image animation, discussed earlier in this chapter:

let anim = CAKeyframeAnimation(keyPath:#keyPath(CALayer.contents))
anim.values = self.images.map {$0.cgImage!}
anim.keyTimes = [0.0, 0.25, 0.5, 0.75, 1.0]
anim.calculationMode = .discrete
anim.duration = 1.5
anim.repeatCount = .greatestFiniteMagnitude
self.sprite.add(anim, forKey:nil) // sprite is a CALayer

Making a Property Animatable

So far, we’ve been animating built-in animatable properties. If you define your own property on a CALayer subclass, you can easily make that property animatable through a CAPropertyAnimation. For example, here we animate the increase or decrease in a CALayer subclass property called thickness, using essentially the pattern for explicit animation that we’ve already developed:

let lay = self.v.layer as! MyLayer
let cur = lay.thickness
let val : CGFloat = cur == 10 ? 0 : 10
lay.thickness = val
let ba = CABasicAnimation(keyPath:#keyPath(MyLayer.thickness))
ba.fromValue = cur
lay.add(ba, forKey:nil)

To make our layer responsive to such a command, it needs a thickness property (obviously), and it must return true from the class method needsDisplay(forKey:) for this property:

class MyLayer : CALayer {
    @objc var thickness : CGFloat = 0
    override class func needsDisplay(forKey key: String) -> Bool {
        if key == #keyPath(thickness) {
            return true
        }
        return super.needsDisplay(forKey:key)
    }
}

Returning true from needsDisplay(forKey:) causes this layer to be redisplayed repeatedly as the thickness property changes. So if we want to see the animation, this layer also needs to draw itself in some way that depends on the thickness property. Here, I’ll implement the layer’s draw(in:) to make thickness the thickness of the black border around a red rectangle:

override func draw(in con: CGContext) {
    let r = self.bounds.insetBy(dx:20, dy:20)
    con.setFillColor(UIColor.red.cgColor)
    con.fill(r)
    con.setLineWidth(self.thickness)
    con.stroke(r)
}

At every frame of the animation, draw(in:) is called, and because the thickness value differs at each step, it appears animated.

We have made MyLayer’s thickness property animatable when using explicit layer animation, but it would be even cooler to make it animatable when using implicit layer animation (that is, when setting lay.thickness directly). Later in this chapter, I’ll show how to do that.

Tip

No law says that you have to draw in your implementation of draw(in:)! Consider layer animation more abstractly as a way of getting the runtime to calculate and send you a series of timed interpolated values, and consider draw(in:) as merely a signal that a new interpolated value has arrived. You can do whatever you like with those values; the possibilities are limitless.

Grouped Animations

A grouped animation (CAAnimationGroup) combines multiple animations into one, by means of its animations property (an array of animations). By delaying and timing the various component animations, complex effects can be achieved.

A CAAnimationGroup is itself an animation; it is a CAAnimation subclass, so it has a duration and other animation features. Think of the CAAnimationGroup as the parent, and its animations as its children. Then the children inherit default property values from their parent. Thus, for example, if you don’t set a child’s duration explicitly, it will inherit the parent’s duration.

Let’s use a grouped animation to construct a sequence where the compass arrow rotates and then waggles. This requires very little modification of code we’ve already written. We express the first animation in its full form, with explicit fromValue and toValue. We postpone the second animation using its beginTime property; notice that we express this in relative terms, as a number of seconds into the parent’s duration, not with respect to CACurrentMediaTime. Finally, we set the overall parent duration to the sum of the child durations, so that it can embrace both of them (failing to do this, and then wondering why some child animations never occur, is a common beginner error):

// capture current value, set final value
let rot = .pi/4.0
CATransaction.setDisableActions(true)
let current = arrow.value(forKeyPath:"transform.rotation.z") as! Double
arrow.setValue(current + rot, forKeyPath:"transform.rotation.z")
// first animation (rotate and clunk)
let anim1 = CABasicAnimation(keyPath:#keyPath(CALayer.transform))
anim1.duration = 0.8
let clunk = CAMediaTimingFunction(controlPoints:0.9, 0.1, 0.7, 0.9)
anim1.timingFunction = clunk
anim1.fromValue = current
anim1.toValue = current + rot
anim1.valueFunction = CAValueFunction(name:.rotateZ)
// second animation (waggle)
var values = [0.0]
let directions = sequence(first:1) {$0 * -1}
let bases = stride(from: 20, to: 60, by: 5)
for (base, dir) in zip(bases, directions) {
    values.append(Double(dir) * .pi / Double(base))
}
values.append(0.0)
let anim2 = CAKeyframeAnimation(keyPath:#keyPath(CALayer.transform))
anim2.values = values
anim2.duration = 0.25
anim2.isAdditive = true
anim2.beginTime = anim1.duration - 0.1
anim2.valueFunction = CAValueFunction(name: .rotateZ)
// group
let group = CAAnimationGroup()
group.animations = [anim1, anim2]
group.duration = anim1.duration + anim2.duration
arrow.add(group, forKey:nil)

In that example, I grouped two animations that animated the same property sequentially. Now let’s do the opposite: we’ll group some animations that animate different properties simultaneously.

I have a small view (self.v), located near the top-right corner of the screen, whose layer contents are a picture of a sailboat facing to the left. I’ll “sail” the boat in a curving path, both down the screen and left and right across the screen, like an extended letter “S” (Figure 4-2). Each time the boat comes to a vertex of the curve, changing direction across the screen, I’ll flip the boat so that it faces the way it’s about to move. At the same time, I’ll constantly rock the boat, so that it always appears to be pitching a little on the waves.

pios 1702
Figure 4-2. A boat and the course she’ll sail

Here’s the first animation, the movement of the boat along its curving path. It illustrates the use of a CAKeyframeAnimation with a CGPath; the calculationMode of .paced ensures an even speed over the whole path. We don’t set an explicit duration because we want to adopt the duration of the group:

let h : CGFloat = 200
let v : CGFloat = 75
let path = CGMutablePath()
var leftright : CGFloat = 1
var next : CGPoint = self.v.layer.position
var pos : CGPoint
path.move(to:CGPoint(next.x, next.y))
for _ in 0 ..< 4 {
    pos = next
    leftright *= -1
    next = CGPoint(pos.x+h*leftright, pos.y+v)
    path.addCurve(to:CGPoint(next.x, next.y),
        control1: CGPoint(pos.x, pos.y+30),
        control2: CGPoint(next.x, next.y-30))
}
let anim1 = CAKeyframeAnimation(keyPath:#keyPath(CALayer.position))
anim1.path = path
anim1.calculationMode = .paced

Here’s the second animation, the reversal of the direction the boat is facing. This is simply a rotation around the y-axis. It’s another CAKeyframeAnimation, but we make no attempt at visually animating this reversal: the calculationMode is .discrete, so that the boat image reversal is a sudden change, as in our earlier “sprite” example. There is one less value than the number of points in our first animation’s path, and the first animation has an even speed, so the reversals take place at each curve apex with no further effort on our part. (If the pacing were more complicated, we could give both the first and the second animation identical keyTimes arrays, to coordinate them.) Once again, we don’t set an explicit duration:

let revs = [0.0, .pi, 0.0, .pi]
let anim2 = CAKeyframeAnimation(keyPath:#keyPath(CALayer.transform))
anim2.values = revs
anim2.valueFunction = CAValueFunction(name:.rotateY)
anim2.calculationMode = .discrete

Here’s the third animation, the rocking of the boat. It has a short duration, and repeats indefinitely:

let pitches = [0.0, .pi/60.0, 0.0, -.pi/60.0, 0.0]
let anim3 = CAKeyframeAnimation(keyPath:#keyPath(CALayer.transform))
anim3.values = pitches
anim3.repeatCount = .greatestFiniteMagnitude
anim3.duration = 0.5
anim3.isAdditive = true
anim3.valueFunction = CAValueFunction(name:.rotateZ)

Finally, we combine the three animations, assigning the group an explicit duration that will be adopted by the first two animations. As we hand the animation over to the layer displaying the boat, we also change the layer’s position to match the final position from the first animation, so that the boat won’t jump back to its original position afterward:

let group = CAAnimationGroup()
group.animations = [anim1, anim2, anim3]
group.duration = 8
self.v.layer.add(group, forKey:nil)
CATransaction.setDisableActions(true)
self.v.layer.position = next

Here are some further CAAnimation properties (from the CAMediaTiming protocol) that come into play especially when animations are grouped:

speed

The ratio between a child’s timescale and the parent’s timescale. For example, if a parent and child have the same duration, but the child’s speed is 1.5, its animation runs one-and-a-half times as fast as the parent.

fillMode

Suppose the child animation begins after the parent animation, or ends before the parent animation, or both. What should happen to the appearance of the property being animated, outside the child animation’s boundaries? The answer depends on the child’s fillMode (CAMediaTimingFillMode):

.removed

The child animation is removed, revealing the layer property at its actual current value whenever the child is not running.

.forwards

The final presentation layer value of the child animation remains afterward.

.backwards

The initial presentation layer value of the child animation appears right from the start.

.both

Combines the previous two.

Freezing an Animation

An animation can be frozen at the level of the layer, with an effect similar to what we did with a property animator earlier. CALayer adopts the CAMediaTiming protocol. Thus, a layer can have a speed. This will affect any animation attached to it. A CALayer with a speed of 2 will play a 10-second animation in 5 seconds. A CALayer with a speed of 0 effectively freezes any animation attached to the layer.

A layer can also have a timeOffset. You can thus change the timeOffset to display any single frame of the layer’s animation.

To illustrate freezing an animation at the CALayer level, let’s explore the animatable path property of a CAShapeLayer. Consider a layer that can display a rectangle or an ellipse or any of the intermediate shapes between them. I can’t imagine what the notion of an intermediate shape between a rectangle or an ellipse may mean, let alone how to draw such an intermediate shape; but thanks to frozen animations, I don’t have to. Here, I’ll construct the CAShapeLayer, add it to the interface, give it an animation from a rectangle to an ellipse, and keep a reference to it as a property:

let shape = CAShapeLayer()
shape.frame = v.bounds
v.layer.addSublayer(shape)
shape.fillColor = UIColor.clear.cgColor
shape.strokeColor = UIColor.red.cgColor
let path = CGPath(rect:shape.bounds, transform:nil)
shape.path = path
let path2 = CGPath(ellipseIn:shape.bounds, transform:nil)
let ba = CABasicAnimation(keyPath:#keyPath(CAShapeLayer.path))
ba.duration = 1
ba.fromValue = path
ba.toValue = path2
shape.speed = 0
shape.timeOffset = 0
shape.add(ba, forKey: nil)
self.shape = shape

I’ve added the animation to the layer, but because the layer’s speed is 0, no animation takes place; the rectangle is displayed and that’s all. As in my earlier example, there’s a UISlider in the interface. I’ll respond to the user changing the value of the slider by setting the frame of the animation:

self.shape.timeOffset = Double(slider.value)

Transitions

A layer transition is an animation involving two “copies” of a single layer, in which the second “copy” appears to replace the first. It is described by an instance of CATransition (a CAAnimation subclass), which has these chief properties specifying the animation:

type

Your choices are (CATransitionType):

  • .fade

  • .moveIn

  • .push

  • .reveal

subtype

If the type is not .fade, your choices are (CATransitionSubtype):

  • .fromRight

  • .fromLeft

  • .fromTop

  • .fromBottom

Warning

For historical reasons, the terms bottom and top in the names of the subtype settings have the opposite of their expected meanings.

To understand a layer transition, first implement one without changing anything else about the layer:

let t = CATransition()
t.type = .push
t.subtype = .fromBottom
t.duration = 2
lay.add(t, forKey: nil)

The entire layer exits moving down from its original place while fading away, and another copy of the very same layer enters moving down from above while fading in. If, at the same time, we change something about the layer’s contents, then the old contents will appear to exit downward while the new contents appear to enter from above:

// ... configure the transition as before ...
CATransaction.setDisableActions(true)
lay.contents = UIImage(named: "Smiley")!.cgImage
lay.add(t, forKey: nil)

A common device is for the layer that is to be transitioned to be inside a superlayer that is exactly the same size and whose masksToBounds is true. This confines the visible transition to the bounds of the layer itself. Otherwise, the entering and exiting versions of the layer are visible outside the layer. In Figure 4-3, which shows a smiley face pushing an image of Mars out of the layer, I’ve emphasized this arrangement by giving the superlayer a border as well.

pios 1704
Figure 4-3. A push transition

A transition on a superlayer can happen simultaneously with animation of a sublayer. The animation will be seen to occur on the second “copy” of the layer as it moves into position. This is analogous to the .allowAnimatedContent option for a view animation.

Animations List

The method that asks for an explicit animation to happen is CALayer’s add(_:forKey:). To understand how this method actually works (and what the “key” is), you need to know about a layer’s animations list.

An animation is an object (a CAAnimation) that modifies how a layer is drawn. It does this merely by being attached to the layer; the layer’s drawing mechanism does the rest. A layer maintains a list of animations that are currently in force. To add an animation to this list, you call add(_:forKey:). When the time comes to draw itself, the layer looks through its animations list and draws itself in accordance with whatever animations it finds there. (The list of things the layer must do in order to draw itself is sometimes referred to by the documentation as the render tree.) The order in which animations were added to the list is the order in which they are applied.

The animations list is maintained in a curious way. The list is not exactly a dictionary, but it behaves somewhat like a dictionary. An animation has a key — the forKey: parameter in add(_:forKey:). If an animation with a certain key is added to the list, and an animation with that key is already in the list, the one that is already in the list is removed. Thus a rule is maintained that only one animation with a given key can be in the list at a time — the exclusivity rule. This explains why sometimes ordering an animation can cancel an animation already ordered or in-flight: the two animations had the same key, so the first one was removed. (Additive view animations affecting the same property work around this limitation by giving the additional animations a different key name — for example, "position" and "position-2".)

It is also possible to add an animation with no key (the key is nil); it is then not subject to the exclusivity rule (that is, there can be more than one animation in the list with no key).

The forKey: parameter in add(_:forKey:) is thus not a property name. It could be a property name, but it can be any arbitrary value. Its purpose is to enforce the exclusivity rule. It does not have any meaning with regard to what property a CAPropertyAnimation animates; that is the job of the animation’s keyPath. (Apple’s use of the term “key” in add(_:forKey:) is thus unfortunate and misleading; I wish they had named this method add(_:identifier:) or something like that.)

However, there is a relationship between the “key” in add(_:forKey:) and the keyPath of a CAPropertyAnimation. If a CAPropertyAnimation’s keyPath is nil at the time that it is added to a layer with add(_:forKey:), that keyPath is set to the forKey: value. Thus, you can misuse the forKey: parameter in add(_:forKey:) as a way of specifying what keyPath an animation animates. (Implicit layer animation crucially depends on this fact.)

Warning

I have seen many prominent but misleading examples that use this technique, apparently in the mistaken belief that the “key” in add(_:forKey:) is the way you are supposed to specify what property to animate. This is wrong. Set the animation’s keyPath explicitly (as do all my examples); that’s what it’s for.

You can use the exclusivity rule to your own advantage, to keep your code from stepping on its own feet. Some code of yours might add an animation to the list using a certain key; then later, some other code might come along and correct this, removing that animation and replacing it with another. By using the same key, the second code is easily able to override the first: “You may have been given some other animation with this key, but throw it away; play this one instead.”

In some cases, the key you supply is ignored and a different key is substituted. In particular, the key with which a CATransition is added to the list is always kCATransition (which happens to be "transition"); thus there can be only one transition animation in the list.

You can think of an animation in a layer’s animations list as being the “animation movie” I spoke of at the start of this chapter. As long as an animation is in the list, the movie is present, either waiting to be played or actually playing. An animation that has finished playing is, in general, pointless; the animation should now be removed from the list, as its presence serves no purpose and it imposes an extra burden on the render tree. Therefore, an animation has an isRemovedOnCompletion property, which defaults to true: when the “movie” is over, the animation removes itself from the list.

Warning

You may encounter examples that set isRemovedOnCompletion to false and set the animation’s fillMode to .forwards or .both, as a way of causing the layer to keep the appearance of the last frame of the “animation movie” even after the animation is over, and preventing a property from apparently jumping back to its initial value when the animation ends. This is wrong. The correct approach, as I have explained, is to change the property value to match the final frame of the animation. The proper use of the fillMode is in connection with a child animation within a grouped animation.

You can’t access the entire animations list directly. You can access the key names of the animations in the list, with animationKeys; and you can obtain or remove an animation with a certain key, with animation(forKey:) and removeAnimation(forKey:); but animations with a nil key are inaccessible. You can, however, remove all animations, including animations with a nil key, using removeAllAnimations. When your app is suspended, removeAllAnimations is called on all layers for you; that is why it is possible to suspend an app coherently in the middle of an animation.

If an animation is in-flight when you remove it from the animations list manually, by calling removeAllAnimations or removeAnimation(forKey:), it will stop; however, that doesn’t happen until the next redraw moment. You might be able to work around this, if you need an animation to be removed immediately, by wrapping the call in an explicit transaction block.

Actions

For the sake of completeness, I will now explain how implicit animation really works — that is, how implicit animation is turned into explicit animation behind the scenes. The basis of implicit animation is the action mechanism. Feel free to skip this section if you don’t want to get into the under-the-hood nitty-gritty of implicit animation. This section is not, however, merely an academic exercise in reverse engineering; your code can hook into the action mechanism to change the behavior of implicit animation in interesting ways.

What an Action Is

An action is an object that adopts the CAAction protocol. This means simply that it implements run(forKey:object:arguments:). The action object could do anything in response to this message. The notion of an action is completely general. The only built-in class that adopts the CAAction protocol is CAAnimation, but in fact the action object doesn’t have to be an animation — it doesn’t even have to perform an animation.

You would never send run(forKey:object:arguments:) to an object directly. Rather, this message is sent to an action object for you, as the basis of implicit animation. The key is the property that was set, and the object is the layer whose property was set.

What an animation does when it receives run(forKey:object:arguments:) is to assume that the object: is a layer, and to add itself to that layer’s animations list. Thus, for an animation, receiving the run(forKey:object:arguments:) message is like being told: “Play yourself!”

This is where the rule comes into play, which I mentioned earlier, that if an animation’s keyPath is nil, the key by which the animation is assigned to a layer’s animations list is used as the keyPath. When an animation is sent run(forKey:object:arguments:), it calls add(_:forKey:) to add itself to the layer’s animation’s list, using the name of the property as the key. The animation’s keyPath for an implicit layer animation is usually nil, so the animation’s keyPath winds up being set to the same key! That is how the property that you set ends up being the property that is animated.

Action Search

When you set a property of a layer, you trigger the action search: the layer searches for an action object (a CAAction) to which it can send the run(forKey:object:arguments:) message. The procedure by which the layer searches for this object is quite elaborate.

The search for an action object begins when something causes the layer to be sent the action(forKey:) message. Three sorts of event can cause this to happen:

  • A CALayer property is set — by calling the setter method explicitly, by setting the property itself, or by means of setValue(_:forKey:). All animatable properties, and indeed most (or all) other built-in CALayer properties, will call action(forKey:) in response to being set. In addition:

    • Setting a layer’s frame property sets its position and bounds and calls action(forKey:) for the "position" and "bounds" keys.

    • Calling a layer’s setAffineTransform(_:) method sets its transform and calls action(forKey:) for the "transform" key.

    • You can configure a custom property to call action(forKey:) by designating it as @NSManaged, as I’ll demonstrate later in this chapter.

  • The layer is sent setValue(_:forKey:) with a key that is not a property, because CALayer’s setValue(_:forUndefinedKey:), by default, calls action(forKey:).

  • Various other miscellaneous types of event take place, such as the layer being added to the interface. I’ll give some examples later.

We are now in a position to understand how CATransaction.setDisableActions(true) works behind the scenes: it prevents the action(forKey:) message from being sent.

At each stage of the action search, the following rules are obeyed regarding what is returned from that stage of the search:

An action object

If an action object is produced, that is the end of the search. The action mechanism sends that action object the run(forKey:object:arguments:) message; if this an animation, the animation responds by adding itself to the layer’s animations list.

NSNull()

If NSNull() is produced, that is the end of the search. There will be no implicit animation; NSNull() means, “Do nothing and stop searching.”

nil

If nil is produced, the search continues to the next stage.

The action search proceeds by stages, as follows:

  1. The layer’s action(forKey:) might terminate the search before it even starts. The layer will do this if it is the underlying layer of a view, or if the layer is not part of a window’s layer hierarchy. In such a case, there should be no implicit animation, so the whole mechanism is nipped in the bud. (This stage is special in that a returned value of nil ends the search and no animation takes place.)

  2. If the layer has a delegate that implements action(for:forKey:), that message is sent to the delegate, with this layer as the first parameter and the property name as the key. If an action object or NSNull() is returned, the search ends.

  3. The layer has a property called actions, which is a dictionary. If there is an entry in this dictionary with the given key, that value is used, and the search ends.

  4. The layer has a property called style, which is a dictionary. If there is an entry in this dictionary with the key actions, it is assumed to be a dictionary; if this actions dictionary has an entry with the given key, that value is used, and the search ends. Otherwise, if there is an entry in the style dictionary called style, the same search is performed within it, and so on recursively until either an actions entry with the given key is found (the search ends) or there are no more style entries (the search continues).

    (If the style dictionary sounds profoundly weird, that’s because it is profoundly weird. It is actually a special case of a larger, separate mechanism, which is also profoundly weird, having to do not with actions, but with a CALayer’s implementation of KVC. When you call value(forKey:) on a layer, if the key is undefined by the layer itself, the style dictionary is consulted. I have never written or seen code that uses this mechanism for anything.)

  5. The layer’s class is sent defaultAction(forKey:), with the property name as the key. If an action object or NSNull() is returned, the search ends.

  6. If the search reaches this last stage, a default animation is supplied, as appropriate. For a property animation, this is a plain vanilla CABasicAnimation.

Hooking Into the Action Search

You can affect the action search at any of its various stages to modify what happens when the search is triggered. This is where the fun begins!

For example, you can turn off implicit animation for some particular property. One way would be to return nil from action(forKey:) itself, in a CALayer subclass. Here’s the code from a CALayer subclass that doesn’t animate its position property (but does animate its other properties normally):

override func action(forKey key: String) -> CAAction? {
    if key == #keyPath(position) {
        return nil
    }
    return super.action(forKey:key)
}

For more flexibility, we can take advantage of the fact that a CALayer acts like a dictionary, allowing us to set an arbitrary key’s value. We’ll embed a switch in our CALayer subclass that we can use to turn implicit position animation on and off at will:

override func action(forKey key: String) -> CAAction? {
    if key == #keyPath(position) {
        if self.value(forKey:"suppressPositionAnimation") != nil {
            return nil
        }
    }
    return super.action(forKey:key)
}

To turn off implicit position animation for an instance of this layer, we set its "suppressPositionAnimation" key to a non-nil value:

layer.setValue(true, forKey:"suppressPositionAnimation")

Another possibility is to cause some stage of the search to produce an action object of your own. You would then be affecting how implicit animation behaves.

Let’s say we want a certain layer’s duration for an implicit position animation to be 5 seconds. We can achieve this with a minimally configured animation, like this:

let ba = CABasicAnimation()
ba.duration = 5

The idea now is to situate this animation where it will be produced by the action search for the "position" key. We could, for instance, put it into the layer’s actions dictionary:

layer.actions = ["position": ba]

The only property of this animation that we have set is its duration; that setting, however, is final. Although animation properties that you don’t set can be set through CATransaction, in the usual manner for implicit property animation, animation properties that you do set can not be overridden through CATransaction. Thus, when we set this layer’s position, if an implicit animation results, its duration is 5 seconds, even if we try to change it through CATransaction:

CATransaction.setAnimationDuration(1.5) // won't work
layer.position = CGPoint(100,100) // animated, takes 5 seconds

Storing an animation in the actions dictionary, however, is a somewhat inflexible way to hook into the action search. If we have to write our animation beforehand, we know nothing about the layer’s starting and ending values for the changed property. A much more powerful approach is to make our action object a custom CAAction object — because in that case, it will be sent run(forKey:...), and we can construct and run an animation now, when we are in direct contact with the layer to be animated. Here’s a barebones version of such an object:

class MyAction : NSObject, CAAction {
    func run(forKey event: String, object anObject: Any,
        arguments dict: [AnyHashable : Any]?) {
            let anim = CABasicAnimation(keyPath: event)
            anim.duration = 5
            let lay = anObject as! CALayer
            let newP = lay.value(forKey:event)
            let oldP = lay.presentation()!.value(forKey:event)
            lay.add(anim, forKey:nil)
    }
}

The idea is that a MyAction instance would then be the action object that we store in the actions dictionary:

layer.actions = ["position": MyAction()]

Our custom CAAction object, MyAction, doesn’t do anything very interesting — but it could. That’s the point. As the code demonstrates, we have access to the name of the animated property (event), the old value of that property (from the layer’s presentation layer), and the new value of that property (from the layer itself). We are thus free to configure the animation in all sorts of ways. In fact, we can add more than one animation to the layer, or a group animation. We don’t even have to add an animation to the layer! We are free to interpret the setting of this property in any way we like.

Here’s a modification of our MyAction object that creates and runs a keyframe animation that “waggles” as it goes from the start value to the end value:

class MyWagglePositionAction : NSObject, CAAction {
    func run(forKey event: String, object anObject: Any,
        arguments dict: [AnyHashable : Any]?) {
            let lay = anObject as! CALayer
            let newP = lay.value(forKey:event) as! CGPoint
            let oldP = lay.presentation()!.value(forKey:event) as! CGPoint
            let d = sqrt(pow(oldP.x - newP.x, 2) + pow(oldP.y - newP.y, 2))
            let r = Double(d/3.0)
            let theta = Double(atan2(newP.y - oldP.y, newP.x - oldP.x))
            let wag = 10 * .pi/180.0
            let p1 = CGPoint(
                oldP.x + CGFloat(r*cos(theta+wag)),
                oldP.y + CGFloat(r*sin(theta+wag)))
            let p2 = CGPoint(
                oldP.x + CGFloat(r*2*cos(theta-wag)),
                oldP.y + CGFloat(r*2*sin(theta-wag)))
            let anim = CAKeyframeAnimation(keyPath: event)
            anim.values = [oldP,p1,p2,newP]
            anim.calculationMode = .cubic
            lay.add(anim, forKey:nil)
    }
}

By adding this CAAction object to a layer’s actions dictionary under the "position" key, we have created a CALayer that waggles when its position property is set. Our CAAction doesn’t set the animation’s duration, so our own call to CATransaction’s setAnimationDuration(_:) works. The power of this mechanism is simply staggering. We can modify any layer in this way — even one that doesn’t belong to us.

Instead of modifying the layer’s actions dictionary, we could hook into the action search by setting the layer’s delegate to an instance that responds to action(for:forKey:). This has the advantage of serving as a single locus that can do different things depending on what the layer is and what the key is. Here’s an implementation that does exactly what the actions dictionary did — it returns an instance of our custom CAAction object, so that setting the layer’s position waggles it into place:

func action(for layer: CALayer, forKey key: String) -> CAAction? {
    if key == #keyPath(CALayer.position) {
        return MyWagglePositionAction()
    }
}

Finally, I’ll demonstrate overriding defaultAction(forKey:). This code would go into a CALayer subclass; setting this layer’s contents will automatically trigger a push transition from the left:

override class func defaultAction(forKey key: String) -> CAAction? {
    if key == #keyPath(contents) {
        let tr = CATransition()
        tr.type = .push
        tr.subtype = .fromLeft
        return tr
    }
    return super.defaultAction(forKey:key)
}
Tip

Both the delegate’s action(for:forKey:) and the subclass’s defaultAction(forKey:) are declared as returning a CAAction. Therefore, to return NSNull() from your implemention of one of these methods, you’ll need to cast it to CAAction to quiet the compiler; you’re lying (NSNull does not adopt the CAAction protocol), but it doesn’t matter.

Making a Custom Property Implicitly Animatable

Earlier in this chapter, we made a custom layer’s thickness property animatable through explicit layer animation. Now that we know how implicit layer animation works, we can make our layer’s thickness property animatable through implicit animation as well. Thus, we will be able to animate our layer’s thickness with code like this:

let lay = self.v.layer as! MyLayer
let cur = lay.thickness
let val : CGFloat = cur == 10 ? 0 : 10
lay.thickness = val // implicit animation

We have already implemented needsDisplay(forKey:) to return true for the "thickness" key, and we have provided an appropriate draw(in:) implementation. Now we’ll add two further pieces of the puzzle. As we now know, to make our MyLayer class respond to direct setting of a property, we need to hook into the action search and return a CAAction. The obvious place to do this is in the layer itself, at the very start of the action search, in an action(forKey:) implementation:

override func action(forKey key: String) -> CAAction? {
    if key == #keyPath(thickness) {
        let ba = CABasicAnimation(keyPath: key)
        ba.fromValue = self.presentation()!.value(forKey:key)
        return ba
    }
    return super.action(forKey:key)
}

Finally, we must declare MyLayer’s thickness property @NSManaged. Otherwise, action(forKey:) won’t be called in the first place and the action search will never happen:

class MyLayer : CALayer {
    @NSManaged var thickness : CGFloat
    // ...
}
Tip

The @NSManaged declaration invites Cocoa to generate and dynamically inject getter and setter accessors into our layer class; it is the equivalent of Objective-C’s @dynamic (and is completely different from Swift’s dynamic).

Nonproperty Actions

An action search is also triggered when a layer is added to a superlayer (key kCAOnOrderIn) and when a layer’s sublayers are changed by adding or removing a sublayer (key "sublayers").

Warning

These triggers and their keys are incorrectly described in Apple’s documentation (and headers).

In this example, we use our layer’s delegate so that when our layer is added to a superlayer, it will “pop” into view:

let layer = CALayer()
// ... configure layer here ...
layer.delegate = self
self.view.layer.addSublayer(layer)

In the layer’s delegate (self), we implement the actual animation as a group animation, fading the layer quickly in from an opacity of 0 and at the same time scaling its transform to make it momentarily appear a little larger:

func action(for layer: CALayer, forKey key: String) -> CAAction? {
    if key == kCAOnOrderIn {
        let anim1 = CABasicAnimation(keyPath:#keyPath(CALayer.opacity))
        anim1.fromValue = 0.0
        anim1.toValue = layer.opacity
        let anim2 = CABasicAnimation(keyPath:#keyPath(CALayer.transform))
        anim2.toValue = CATransform3DScale(layer.transform, 1.2, 1.2, 1.0)
        anim2.autoreverses = true
        anim2.duration = 0.1
        let group = CAAnimationGroup()
        group.animations = [anim1, anim2]
        group.duration = 0.2
        return group
    }
}

The documentation says that when a layer is removed from a superlayer, an action is sought under the key kCAOnOrderOut. This is true but useless, because by the time the action is sought, the layer has already been removed from the superlayer, so returning an animation has no visible effect. A possible workaround is to trigger the animation in some other way (and remove the layer afterward, if desired).

Recall, for example, that an action search is triggered when an arbitrary key is set on a layer. Let’s implement the key "farewell" so that it shrinks and fades the layer and then removes it from its superlayer:

layer.delegate = self
layer.setValue("", forKey:"farewell")

The supplier of the action object — in this case, the layer’s delegate — returns the shrink-and-fade animation; it also sets itself as that animation’s delegate, and removes the layer when the animation ends:

func action(for layer: CALayer, forKey key: String) -> CAAction? {
    if key == "farewell" {
        let anim1 = CABasicAnimation(keyPath:#keyPath(CALayer.opacity))
        anim1.fromValue = layer.opacity
        anim1.toValue = 0.0
        let anim2 = CABasicAnimation(keyPath:#keyPath(CALayer.transform))
        anim2.toValue = CATransform3DScale(layer.transform, 0.1, 0.1, 1.0)
        let group = CAAnimationGroup()
        group.animations = [anim1, anim2]
        group.duration = 0.2
        group.delegate = self
        group.setValue(layer, forKey:"remove")
        layer.opacity = 0
        return group
    }
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
    if let layer = anim.value(forKey:"remove") as? CALayer {
        layer.removeFromSuperlayer()
    }
}

Emitter Layers

Emitter layers (CAEmitterLayer) are, to some extent, on a par with animated images: once you’ve set up an emitter layer, it just sits there animating all by itself. The nature of this animation is rather narrow: an emitter layer emits particles, which are CAEmitterCell instances. However, by clever setting of the properties of an emitter layer and its emitter cells, you can achieve some astonishing effects. Moreover, the animation is itself animatable using Core Animation.

Here are some useful basic properties of a CAEmitterCell:

contents, contentsRect

These are modeled after the eponymous CALayer properties, although CAEmitterCell is not a CALayer subclass; so, respectively, an image (a CGImage) and a CGRect specifying a region of that image. They define the image that a cell will portray.

birthrate, lifetime

How many cells per second should be emitted, and how many seconds each cell should live before vanishing, respectively.

velocity

The speed at which a cell moves. The unit of measurement is not documented; perhaps it’s points per second.

emissionLatitude, emissionLongitude

The angle at which the cell is emitted from the emitter, as a variation from the perpendicular. Longitude is an angle within the plane; latitude is an angle out of the plane.

So, here’s code to create a very elementary emitter cell:

// make a gray circle image
let r = UIGraphicsImageRenderer(size:CGSize(10,10))
let im = r.image {
    ctx in let con = ctx.cgContext
    con.addEllipse(in:CGRect(0,0,10,10))
    con.setFillColor(UIColor.gray.cgColor)
    con.fillPath()
}
// make a cell with that image
let cell = CAEmitterCell()
cell.contentsScale = UIScreen.main.scale
cell.birthRate = 5
cell.lifetime = 1
cell.velocity = 100
cell.contents = im.cgImage

The result is that little gray circles should be emitted slowly and steadily, five per second, each one vanishing in one second. Now we need an emitter layer from which these circles are to be emitted. Here are some basic CAEmitterLayer properties (beyond those it inherits from CALayer); these define an imaginary object, an emitter, that will be producing the emitter cells:

emitterPosition

The point at which the emitter should be located, in superlayer coordinates. You can optionally add a third dimension to this point, emitterZPosition.

emitterSize

The size of the emitter.

emitterShape

The shape of the emitter. The dimensions of the shape depend on the emitter’s size; the cuboid shape depends also on a third size dimension, emitterDepth. Your choices are (CAEmitterLayerEmitterShape):

  • .point

  • .line

  • .rectangle

  • .cuboid

  • .circle

  • .sphere

emitterMode

The region of the shape from which cells should be emitted. Your choices are (CAEmitterLayerEmitterMode):

  • .points

  • .outline

  • .surface

  • .volume

Let’s start with the simplest possible case, a single point emitter:

let emit = CAEmitterLayer()
emit.emitterPosition = CGPoint(30,100)
emit.emitterShape = .point
emit.emitterMode = .points

We tell the emitter what types of cell to emit by assigning those cells to its emitterCells property (an array of CAEmitterCell). We then add the emitter to our interface, and presto, it starts emitting:

emit.emitterCells = [cell]
self.view.layer.addSublayer(emit)

The result is a constant stream of gray circles emitted from the point (30.0,100.0), each circle marching steadily to the right and vanishing after one second (Figure 4-4).

pios 1705
Figure 4-4. A really boring emitter layer

Now that we’ve succeeded in creating a boring emitter layer, we can start to vary some parameters. The emissionRange defines a cone in which cells will be emitted; if we increase the birthRate and widen the emissionRange, we get something that looks like a stream shooting from a water hose:

cell.birthRate = 100
cell.lifetime = 1.5
cell.velocity = 100
cell.emissionRange = .pi/5.0

In addition, as the cell moves, it can be made to accelerate (or decelerate) in each dimension, using its xAcceleration, yAcceleration, and zAcceleration properties. Here, we turn the stream into a falling cascade, like a waterfall coming from the left:

cell.xAcceleration = -40
cell.yAcceleration = 200

All aspects of cell behavior can be made to vary randomly, using the following CAEmitterCell properties:

lifetimeRange, velocityRange

How much the lifetime and velocity values are allowed to vary randomly for different cells.

scale
scaleRange, scaleSpeed

The scale alters the size of the cell; the range and speed determine how far and how rapidly this size alteration is allowed to change over the lifetime of each cell.

color
redRange, greenRange, blueRange, alphaRange
redSpeed, greenSpeed, blueSpeed, alphaSpeed

The color is painted in accordance with the opacity of the cell’s contents image; it combines with the image’s color, so if we want the color stated here to appear in full purity, our contents image should use only white. The range and speed determine how far and how rapidly each color component is to change.

spin
spinRange

The spin is a rotational speed (in radians per second); its range determines how far this speed is allowed to change over the lifetime of each cell.

Here we add some variation so that the circles behave a little more independently of one another. Some live longer than others, some come out of the emitter faster than others. And they all start out a shade of blue, but change to a shade of green about halfway through the stream (Figure 4-5):

cell.lifetimeRange = 0.4
cell.velocityRange = 20
cell.scaleRange = 0.2
cell.scaleSpeed = 0.2
cell.color = UIColor.blue.cgColor
cell.greenRange = 0.5
cell.greenSpeed = 0.75
pios 1706
Figure 4-5. An emitter layer that makes a sort of waterfall

Once the emitter layer is in place and animating, you can change its parameters and the parameters of its emitter cells through key–value coding on the emitter layer. You can access the emitter cells through the emitter layer’s "emitterCells" key path; to specify a cell type, use its name property (which you’ll have to have assigned earlier) as the next piece of the key path. For example, suppose we’ve set cell.name to "circle"; now we’ll change the cell’s greenSpeed so that each cell changes from blue to green much earlier in its lifetime:

emit.setValue(3.0, forKeyPath:"emitterCells.circle.greenSpeed")

The significance of this is that such changes can themselves be animated! Here, we’ll attach to the emitter layer a repeating animation that causes our cell’s greenSpeed to move slowly back and forth between two values. The result is that the stream varies, over time, between being mostly blue and mostly green:

let key = "emitterCells.circle.greenSpeed"
let ba = CABasicAnimation(keyPath:key)
ba.fromValue = -1.0
ba.toValue = 3.0
ba.duration = 4
ba.autoreverses = true
ba.repeatCount = .greatestFiniteMagnitude
emit.add(ba, forKey:nil)

A CAEmitterCell can itself function as an emitter — that is, it can have cells of its own. Both CAEmitterLayer and CAEmitterCell conform to the CAMediaTiming protocol, and their beginTime and duration properties can be used to govern their times of operation, much as in a grouped animation. For example, this code causes our existing waterfall to spray tiny droplets in the region of the “nozzle” (the emitter):

let cell2 = CAEmitterCell()
cell.emitterCells = [cell2]
cell2.contents = im.cgImage
cell2.emissionRange = .pi
cell2.birthRate = 200
cell2.lifetime = 0.4
cell2.velocity = 200
cell2.scale = 0.2
cell2.beginTime = 0.04
cell2.duration = 0.2

But if we change the beginTime to be larger (hence later), the tiny droplets happen near the bottom of the cascade. We must also increase the duration, or stop setting it altogether, since if the duration is less than the beginTime, no emission takes place at all (Figure 4-6):

cell2.beginTime = 1.4
cell2.duration = 0.4
pios 1707
Figure 4-6. The waterfall makes a kind of splash

We can also alter the picture by changing the behavior of the emitter itself. This change turns the emitter into a line, so that our cascade becomes broader (more like Niagara Falls):

emit.emitterPosition = CGPoint(100,25)
emit.emitterSize = CGSize(100,100)
emit.emitterShape = .line
emit.emitterMode = .outline
cell.emissionLongitude = 3 * .pi/4

There’s more to know about emitter layers and emitter cells, but at this point you know enough to understand Apple’s sample code simulating such things as fire and smoke and pyrotechnics, and you can explore further on your own.

CIFilter Transitions

Core Image filters (Chapter 2) include transitions. You supply two images and a frame time between 0 and 1; the filter supplies the corresponding frame of a one-second animation transitioning from the first image to the second. For example, Figure 4-7 shows the frame at frame time 0.75 for a starburst transition from a solid red image to a photo of me. (You don’t see the photo of me, because this transition, by default, “explodes” the first image to white first, and then quickly fades to the second image.)

pios 1708
Figure 4-7. Midway through a starburst transition

Animating a Core Image transition filter is up to you. Thus we need a way of rapidly calling the same method repeatedly; in that method, we’ll request and draw each frame of the transition. This could be a job for a Timer, but a better way is to use a display link (CADisplayLink), a form of timer that’s linked directly to the refreshing of the display (hence the name). The display refresh rate is hardware-dependent, but is typically every sixtieth of a second or faster; UIScreen.maximumFramesPerSecond will tell you the nominal value, and the nominal time between refreshes is the display link’s duration.

Tip

For the smoothest display of a Core Image transition filter animation with the least strain on the device’s CPU, you would use Metal. But that’s outside the scope of this book.

Like a timer, the display link calls a designated method of ours every time it fires. We can slow the rate of calls by setting the display link’s preferredFramesPerSecond. We can learn the exact time when the display link last fired by querying its timestamp, and that’s the best way to decide what frame needs displaying now.

In this example, I’ll display the animation in a view’s layer. We initialize ahead of time, in properties, everything we’ll need later to obtain an output image for a given frame of the transition — the CIFilter, the image’s extent, and the CIContext. We also have a timestamp property, which we initialize as well:

let moi = CIImage(image:UIImage(named:"moi")!)!
self.moiextent = moi.extent
let col = CIFilter(name:"CIConstantColorGenerator")!
let cicol = CIColor(color:.red)
col.setValue(cicol, forKey:"inputColor")
let colorimage = col.value(forKey:"outputImage") as! CIImage
let tran = CIFilter(name:"CIFlashTransition")!
tran.setValue(colorimage, forKey:"inputImage")
tran.setValue(moi, forKey:"inputTargetImage")
let center = CIVector(x:self.moiextent.width/2.0, y:self.moiextent.height/2.0)
tran.setValue(center, forKey:"inputCenter")
self.tran = tran
self.timestamp = 0.0 // signal that we are starting
self.context = CIContext()

We create the display link, setting it to call into our nextFrame method, and set it going by adding it to the main run loop, which retains it:

let link = CADisplayLink(target:self, selector:#selector(self.nextFrame))
link.add(to:.main, forMode:.default)

Our nextFrame(_:) method is called with the display link as parameter (sender). We store the initial timestamp in our property, and use the difference between that and each successive timestamp value to calculate our desired frame. We ask the filter for the corresponding image and display it. When the frame value exceeds 1, the animation is over and we invalidate the display link (just like a repeating timer), which releases it from the run loop:

let SCALE = 1.0
@objc func nextFrame(_ sender:CADisplayLink) {
    if self.timestamp < 0.01 { // pick up and store first timestamp
        self.timestamp = sender.timestamp
        self.frame = 0.0
    } else { // calculate frame
        self.frame = (sender.timestamp - self.timestamp) * SCALE
    }
    sender.isPaused = true // defend against frame loss
    self.tran.setValue(self.frame, forKey:"inputTime")
    let moi = self.context.createCGImage(
        tran.outputImage!, from:self.moiextent)
    CATransaction.setDisableActions(true)
    self.v.layer.contents = moi
    if self.frame > 1.0 {
        sender.invalidate()
    }
    sender.isPaused = false
}

I have surrounded the time-consuming calculation and drawing of the image with calls to the display link’s isPaused property, in case the calculation time exceeds the time between screen refreshes; perhaps this isn’t necessary, but it can’t hurt. Our animation occupies one second; changing that value is merely a matter of multiplying by a different scale value when we set our frame property.

UIKit Dynamics

UIKit dynamics comprises a suite of classes supplying a convenient API for animating views in a manner reminiscent of real-world physical behavior. For example, views can be subjected to gravity, collisions, bouncing, and transient forces, with effects that would otherwise be difficult to achieve.

UIKit dynamics should not be treated as a game engine. It is deliberately quite cartoony and simple, animating only the position (center) and rotation transform of views within a flat two-dimensional space. UIKit dynamics relies on CADisplayLink, and the calculation of each frame takes place on the main thread (not on the animation server’s background thread). There’s no “animation movie” and no distinct presentation layer; the views really are being repositioned in real time. Thus, UIKit Dynamics is not intended for extended use; it is a way of momentarily emphasizing or clarifying functional transformations of your interface.

The Dynamics Stack

Implementing UIKit dynamics involves configuring a “stack” of three things:

A dynamic animator

A dynamic animator, a UIDynamicAnimator instance, is the ruler of the physics world you are creating. It has a reference view, whose bounds define the coordinate system of the animator’s world. A view to be animated must be a subview of the reference view (though it does not have to be within the reference view’s bounds). Retaining the animator is up to you, typically with an instance property. It’s fine for an animator to sit empty until you need it; an animator whose world is empty (or at rest) is not running, and occupies no processor time.

A behavior

A UIDynamicBehavior is a rule describing how a view should behave. You’ll typically use a built-in subclass, such as UIGravityBehavior or UICollisionBehavior. You configure the behavior and add it to the animator; an animator has methods and properties for managing its behaviors, such as addBehavior(_:), behaviors, removeBehavior(_:), and removeAllBehaviors. A behavior’s configuration can be changed, and behaviors can be added to and removed from an animator, even while an animation is in progress.

An item

An item is any object that implements the UIDynamicItem protocol. A UIView is such an object! You add a UIView (one that’s a subview of your animator’s reference view) to a behavior (one that belongs to that animator) — and at that moment, the view comes under the influence of that behavior. If this behavior is one that causes motion, and if no other behaviors prevent, the view will now move (the animator is running).

Some behaviors can accept multiple items, and have methods and properties such as addItem(_:), items, and removeItem(_:). Others can have just one or two items and must be initialized with these from the outset.

A UIDynamicItemGroup is a way of combining multiple items to form a single item. Its only property is its items. You apply behaviors to the resulting grouped item, not to the subitems that it comprises. Those subitems maintain their physical relationship to one another. For purposes of collisions, the boundaries of the individual subitems are respected.

That’s sufficient to get started, so let’s try it! First I’ll create my animator and store it in a property:

self.anim = UIDynamicAnimator(referenceView: self.view)

Now I’ll cause an existing subview of self.view (a UIImageView, self.iv) to drop off the screen, under the influence of gravity. I create a UIGravityBehavior, add it to the animator, and add self.iv to it:

let grav = UIGravityBehavior()
self.anim.addBehavior(grav)
grav.addItem(self.iv)

As a result, self.iv comes under the influence of gravity and is now animated downward off the screen. (A UIGravityBehavior object has properties configuring the strength and direction of gravity, but I’ve left them here at their defaults.)

An immediate concern is that our view falls forever. This is a serious waste of memory and processing power. If we no longer need the view after it has left the screen, we should take it out of the influence of UIKit dynamics by removing it from any behaviors to which it belongs (and we can also remove it from its superview). One way to do this is by removing from the animator any behaviors that are no longer needed. In our simple example, where the animator’s entire world contains just this one item, it will be sufficient to call removeAllBehaviors.

But how will we know when the view is off the screen? A UIDynamicBehavior can be assigned an action function, which is called repeatedly as the animator drives the animation. I’ll configure our gravity behavior’s action function to check whether self.iv is still within the bounds of the reference view, by calling the animator’s items(in:) method. Actually, items(in:) returns an array of UIDynamicItem, but I want an array of UIView, so I like to have on hand a UIDynamicAnimator extension that will cast down safely:

extension UIDynamicAnimator {
    func views(in rect: CGRect) -> [UIView] {
        let nsitems = self.items(in: rect) as NSArray
        return nsitems.compactMap {$0 as? UIView}
    }
}

Here’s my first attempt:

grav.action = {
    let items = self.anim.views(in:self.view.bounds)
    let ix = items.firstIndex(of:self.iv)
    if ix == nil {
        self.anim.removeAllBehaviors()
        self.iv.removeFromSuperview()
    }
}

This works in the sense that, after the image view leaves the screen, the image view is removed from the window and the animation stops. Unfortunately, there is also a memory leak: neither the image view nor the gravity behavior has been released. One solution is, in grav.action, to set self.anim (the animator property) to nil, thus breaking the retain cycle. This is a perfectly appropriate solution if, as here, we no longer need the animator for anything; a UIDynamicAnimator is a lightweight object and can very reasonably come into existence only for as long as we need to run an animation. Another possibility is to use delayed performance; even a delay of 0 solves the problem, presumably because the behavior’s action function is no longer running at the time we remove the behavior:

grav.action = {
    let items = self.anim.views(in:self.view.bounds)
    let ix = items.firstIndex(of:self.iv)
    if ix == nil {
        delay(0) {
            self.anim.removeAllBehaviors()
            self.iv.removeFromSuperview()
        }
    }
}

Now let’s add some further behaviors. If falling straight down is too boring, we can add a UIPushBehavior to create a slight rightward impulse to be applied to the view as it begins to fall:

let push = UIPushBehavior(items:[self.iv], mode:.instantaneous)
push.pushDirection = CGVector(1,0)
self.anim.addBehavior(push)

The view now falls in a parabola to the right. Next, let’s add a UICollisionBehavior to make our view strike the “floor” of the screen:

let coll = UICollisionBehavior()
coll.collisionMode = .boundaries
coll.collisionDelegate = self
let b = self.view.bounds
coll.addBoundary(withIdentifier:"floor" as NSString,
    from:CGPoint(b.minX, b.maxY), to:CGPoint(b.maxX, b.maxY))
self.anim.addBehavior(coll)
coll.addItem(self.iv)

The view now falls in a parabola onto the floor of the screen, bounces a tiny bit, and comes to rest. It would be nice if the view bounced a bit more. Characteristics internal to a dynamic item’s physics, such as bounciness (elasticity), are configured by assigning it to a UIDynamicItemBehavior:

let bounce = UIDynamicItemBehavior()
bounce.elasticity = 0.8
self.anim.addBehavior(bounce)
bounce.addItem(self.iv)

Our view now bounces higher; nevertheless, when it hits the floor, it stops moving to the right, so it just bounces repeatedly, less and less, and ends up at rest on the floor. I’d prefer that, after it bounces, it should roll to the right, so that it eventually leaves the screen. Part of the problem here is that, in the mind of the physics engine, our view is not round. We can change that. We’ll have to subclass our view class (UIImageView), and make sure our view is an instance of this subclass:

class MyImageView : UIImageView {
    override var collisionBoundsType: UIDynamicItemCollisionBoundsType {
        return .ellipse
    }
}

Our image view now has the ability to roll. If the image view is portraying a circular image, the effect is quite realistic: the image itself appears to roll to the right after it bounces. However, it isn’t rolling very fast (because we didn’t initially push it very hard). To remedy that, I’ll add some rotational velocity as part of the first bounce. A UICollisionBehavior has a delegate to which it sends messages when a collision occurs. I’ll make self the collision behavior’s delegate, and when the delegate message arrives, I’ll add rotational velocity to the existing dynamic item bounce behavior, so that our view starts spinning clockwise:

func collisionBehavior(_ behavior: UICollisionBehavior,
    beganContactFor item: UIDynamicItem,
    withBoundaryIdentifier identifier: NSCopying?,
    at p: CGPoint) {
        // look for the dynamic item behavior
        let b = self.anim.behaviors
        if let bounce = (b.compactMap {$0 as? UIDynamicItemBehavior}).first {
            let v = bounce.angularVelocity(for:item)
            if v <= 6 {
                bounce.addAngularVelocity(6, for:item)
            }
        }
}

The view now falls in a parabola to the right, strikes the floor, spins clockwise, and bounces off the floor and continues bouncing its way off the right side of the screen.

Custom Behaviors

You will commonly find yourself composing a complex behavior out of a combination of several built-in UIDynamicBehavior subclass instances. It might make sense to express that combination as a single custom UIDynamicBehavior subclass.

To illustrate, I’ll turn the behavior from the previous section into a custom subclass of UIDynamicBehavior. Let’s call it MyDropBounceAndRollBehavior. Now we can apply this behavior to our view, self.iv, very simply:

self.anim.addBehavior(MyDropBounceAndRollBehavior(view:self.iv))

All the work is now done by the MyDropBounceAndRollBehavior instance. I’ve designed it to affect just one view, so its initializer looks like this:

let v : UIView
init(view v:UIView) {
    self.v = v
    super.init()
}

A UIDynamicBehavior receives a reference to its dynamic animator just before being added to it, by implementing willMove(to:), and can refer to it subsequently as self.dynamicAnimator. To incorporate actual behaviors into itself, our custom UIDynamicBehavior subclass creates and configures them, and calls addChildBehavior(_:); it can refer to the array of its child behaviors as self.childBehaviors. When our custom behavior is added to or removed from the dynamic animator, the effect is the same as if its child behaviors themselves were added or removed.

Here is the rest of MyDropBounceAndRollBehavior. Our precautions in the gravity behavior’s action block not to cause a retain cycle are simpler than before; it suffices to designate self as an unowned reference and remove self from the animator explicitly:

override func willMove(to anim: UIDynamicAnimator?) {
    guard let anim = anim else { return }
    let sup = self.v.superview!
    let b = sup.bounds
    let grav = UIGravityBehavior()
    grav.action = { [unowned self] in
        let items = anim.views(in: b)
        if items.firstIndex(of:self.v) == nil {
            anim.removeBehavior(self)
            self.v.removeFromSuperview()
        }
    }
    self.addChildBehavior(grav)
    grav.addItem(self.v)
    let push = UIPushBehavior(items:[self.v], mode:.instantaneous)
    push.pushDirection = CGVector(1,0)
    self.addChildBehavior(push)
    let coll = UICollisionBehavior()
    coll.collisionMode = .boundaries
    coll.collisionDelegate = self
    coll.addBoundary(withIdentifier:"floor" as NSString,
        from: CGPoint(b.minX, b.maxY), to:CGPoint(b.maxX, b.maxY))
    self.addChildBehavior(coll)
    coll.addItem(self.v)
    let bounce = UIDynamicItemBehavior()
    bounce.elasticity = 0.8
    self.addChildBehavior(bounce)
    bounce.addItem(self.v)
}
func collisionBehavior(_ behavior: UICollisionBehavior,
    beganContactFor item: UIDynamicItem,
    withBoundaryIdentifier identifier: NSCopying?,
    at p: CGPoint) {
        // look for the dynamic item behavior
        let b = self.childBehaviors
        if let bounce = (b.compactMap {$0 as? UIDynamicItemBehavior}).first {
            let v = bounce.angularVelocity(for:item)
            if v <= 6 {
                bounce.addAngularVelocity(6, for:item)
            }
        }
}

Animator and Behaviors

Here are some further UIDynamicAnimator methods and properties:

delegate

The delegate (UIDynamicAnimatorDelegate) is sent messages dynamicAnimatorDidPause(_:) and dynamicAnimatorWillResume(_:). The animator is paused when it has nothing to do: it has no dynamic items, or all its dynamic items are at rest.

isRunning

If true, the animator is not paused; some dynamic item is being animated.

elapsedTime

The total time during which this animator has been running since it first started running. The elapsedTime does not increase while the animator is paused, nor is it reset. You might use this in a delegate method or action method to decide that the animation is over.

updateItem(usingCurrentState:)

Once a dynamic item has come under the influence of the animator, the animator is responsible for positioning that dynamic item. If your code manually changes the dynamic item’s position or other relevant attributes, call this method so that the animator can take account of those changes.

The rest of this section surveys the various built-in UIDynamicBehavior subclasses.

Tip

You can turn on a display that reveals visually what the animator is doing, showing its attachment lines and so forth; assuming that self.anim refers to the dynamic animator, you would say:

self.anim.perform(Selector(("setDebugEnabled:")), with:true)

UIDynamicItemBehavior

A UIDynamicItemBehavior doesn’t apply any force or velocity; instead, it is a way of endowing items with internal physical characteristics that will affect how they respond to other dynamic behaviors. Here are some of them:

density

Changes the impulse-resisting mass in relation to size. In other words, when we speak of an item’s mass, we mean a combination of its size and its density.

elasticity

The item’s tendency to bounce on collision.

friction

The item’s tendency to be slowed by sliding past another item.

isAnchored

An anchored item is not affected by forces that would make an item move; thus it remains stationary. This can give you something with friction and elasticity off of which you can bounce and slide other items.

resistance, angularResistance, allowsRotation

The item’s tendency to come to rest unless forces are actively applied. allowsRotation can prevent the item from acquiring any angular velocity at all.

charge

Meaningful only with respect to magnetic and electric fields, which I’ll get to in a moment.

addLinearVelocity(_:for:), linearVelocity(for:)
addAngularVelocity(_:for:), angularVelocity(for:)

Methods for tweaking linear and angular velocity.

UIGravityBehavior

UIGravityBehavior imposes an acceleration on its dynamic items. By default, this acceleration is downward with a magnitude of 1 (arbitrarily defined as 1000 points per second per second). You can customize gravity by changing its gravityDirection (a CGVector) or its angle and magnitude.

UIFieldBehavior

UIFieldBehavior is a generalization of UIGravityBehavior. A field affects any of its items for as long as they are within its area of influence, as described by these properties:

position

The center of the field’s effective area of influence, in reference view coordinates. The default position is CGPoint.zero, the reference view’s top left corner.

region

The shape of the field’s effective area of influence; a UIRegion. The default is that the region is infinite, but you can limit it to a circle by its radius or to a rectangle by its size. More complex region shapes can be achieved by taking the union, intersection, or difference of two regions, or the inverse of a region.

strength

The magnitude of the field. It can be negative to reverse the directionality of the field’s forces.

falloff

Defines a change in strength proportional to the distance from the center.

minimumRadius

Specifies a central circle within which there is no field effect.

direction, smoothness, animationSpeed

Applicable only to those built-in field types that define them.

The built-in field types are obtained by calling a class factory method:

linearGravityField(direction:)

Like UIGravityBehavior. Accelerates the item in the direction of a vector that you supply, proportionally to its mass, the length of the vector, and the strength of the field. The vector is the field’s direction, and can be changed.

velocityField(direction:)

Like UIGravityBehavior, but it doesn’t apply an acceleration (a force) — instead, it applies a constant velocity.

radialGravityField(position:)

Like a point-oriented version of UIGravityBehavior. Accelerates the item toward, or pushes it away from, the field’s designated central point (its position).

springField

Behaves as if there were a spring stretching from the item to the center, so that the item oscillates back and forth across the center until it settles there.

electricField

Behaves like an electric field emanating from the center. The default strength and falloff are both 1. If you set the falloff to 0, then a negatively charged item, all other things being equal, will oscillate endlessly across the center.

magneticField

Behaves like a magnetic field emanating from the center. A moving charged item’s path is bent away from the center.

vortexField

Accelerates the item sideways with respect to the center.

dragField

Reduces the item’s speed.

noiseField(smoothness:animationSpeed:)

Adds random disturbance to the position of the item. The smoothness is between 0 (noisy) and 1 (smooth). The animationSpeed is how many times per second the field should change randomly. Both can be changed in real time.

turbulenceField(smoothness:animationSpeed:)

Like a noise field, but takes the item’s velocity into account.

Think of a field as an infinite grid of CGVectors, with the potential to affect the speed and direction (that is, the velocity) of an item within its borders; at every instant of time the vector applicable to a particular item can be recalculated. You can write a custom field by calling the UIFieldBehavior class method field(evaluationBlock:) with a function that takes the item’s position, velocity, mass, and charge, along with the animator’s elapsed time, and returns a CGVector.

In this (silly) example, we create a delayed drag field: for the first quarter second it does nothing, but then it suddenly switches on and applies the brakes to its items, bringing them to a standstill if they don’t already have enough velocity to escape the region’s boundaries:

let b = UIFieldBehavior.field {
    (beh, pt, v, m, c, t) -> CGVector in
    if t > 0.25 {
        return CGVector(-v.dx, -v.dy)
    }
    return CGVector(0,0)
}

The evaluation function receives the behavior itself as a parameter, so it can consult the behavior’s properties in real time. You can define your own properties by subclassing UIFieldBehavior. If you’re going to do that, you might as well also define your own class factory method to configure and return the custom field. To illustrate, I’ll turn the hard-coded 0.25 delay from the previous example into an instance property:

class MyDelayedFieldBehavior : UIFieldBehavior {
    var delay = 0.0
    class func dragField(delay del:Double) -> Self {
        let f = self.field {
            (beh, pt, v, m, c, t) -> CGVector in
            if t > (beh as! MyDelayedFieldBehavior).delay {
                return CGVector(-v.dx, -v.dy)
            }
            return CGVector(0,0)
        }
        f.delay = del
        return f
    }
}

Here’s an example of creating and configuring our delayed drag field:

let b = MyDelayedFieldBehavior.dragField(delay:0.95)
b.region = UIRegion(size: self.view.bounds.size)
b.position = CGPoint(self.view.bounds.midX, self.view.bounds.midY)
b.addItem(v)
self.anim.addBehavior(b)

UIPushBehavior

UIPushBehavior applies a force either instantaneously or continuously (mode), the latter constituting an acceleration. How this force affects an object depends in part upon the object’s mass. The effect of a push behavior can be toggled with the active property; an instantaneous push is repeated each time the active property is set to true.

To configure a push behavior, set its pushDirection or its angle and magnitude. In addition, a push may be applied at an offset from the center of an item. This will apply an additional angular acceleration. Thus, in my earlier example, I could have started the view spinning clockwise by means of its initial push, like this:

push.setTargetOffsetFromCenter(
    UIOffset(horizontal:0, vertical:-200), for: self.iv)

UICollisionBehavior

UICollisionBehavior watches for collisions either between items belonging to this same behavior or between an item and a boundary (mode). One collision behavior can have multiple items and multiple boundaries. A boundary may be described as a line between two points or as a UIBezierPath, or you can turn the reference view’s bounds into boundaries (setTranslatesReferenceBoundsIntoBoundary(with:)). Boundaries that you create can have an identifier. The collisionDelegate (UICollisionBehaviorDelegate) is called when a collision begins and again when it ends.

How a given collision affects the item(s) involved depends on the physical characteristics of the item(s), which may be configured through a UIDynamicItemBehavior.

A dynamic item, such as a UIView, can have a customized collision boundary, rather than its collision boundary being merely the edges of its frame. You can have a rectangle dictated by the frame, an ellipse dictated by the frame, or a custom shape — a convex counterclockwise simple closed UIBezierPath. The relevant properties, collisionBoundsType and (for a custom shape) collisionBoundingPath, are read-only, so you will have to subclass, as I did in my earlier example.

UISnapBehavior

UISnapBehavior causes one item to snap to one point as if pulled by a spring. Its damping describes how much the item should oscillate as its settles into that point. This is a very simple behavior: the snap occurs immediately when the behavior is added to the animator, and there’s no notification when it’s over.

The snap behavior’s snapPoint is a settable property. Thus, having performed a snap, you can subsequently change the snapPoint and cause another snap to take place.

UIAttachmentBehavior

UIAttachmentBehavior attaches an item to another item or to a point in the reference view, depending on how you initialize it:

  • init(item:attachedTo:)

  • init(item:attachedToAnchor:)

The attachment point is, by default, the item’s center; to change that, there’s a different pair of initializers:

  • init(item:offsetFromCenter:attachedTo:offsetFromCenter:)

  • init(item:offsetFromCenter:attachedToAnchor:)

The attaching medium’s physics are governed by the behavior’s length, frequency, and damping. If the frequency is 0 (the default), the attachment is like a bar; otherwise, and especially if the damping is very small, it is like a spring.

If the attachment is to another item, that item might move. If the attachment is to an anchor, you can move the anchorPoint. When that happens, this item moves too, in accordance with the physics of the attaching medium. An anchorPoint is particularly useful for implementing a draggable view within an animator world, as I’ll demonstrate in the next chapter.

There are several more varieties of attachment:

Limit attachment

A limit attachment is created with this class method:

  • limitAttachment(with:offsetFromCenter:attachedTo:offsetFromCenter:)

It’s like a rope running between two items. Each item can move freely and independently until the length is reached, at which point the moving item drags the other item along.

Fixed attachment

A fixed attachment is created with this class method:

  • fixedAttachment(with:attachedTo:attachmentAnchor:)

It’s as if there are two rods; each rod has an item at one end, with the other ends of the rods being welded together at the anchor point. If one item moves, it must remain at a fixed distance from the anchor, and will tend to rotate around it while pulling it along, at the same time making the other item rotate around the anchor.

Pin attachment

A pin attachment is created with this class method:

  • pinAttachment(with:attachedTo:attachmentAnchor:)

A pin attachment is like a fixed attachment, but instead of the rods being welded together, they are hinged together. Each item is thus free to rotate around the anchor point, at a fixed distance from it, independently, subject to the pin attachment’s frictionTorque which injects resistance into the hinge.

Sliding attachment

A sliding attachment can involve one or two items, and is created with one of these class methods:

  • slidingAttachment(with:attachmentAnchor:axisOfTranslation:)

  • slidingAttachment(with:attachedTo:attachmentAnchor:axisOfTranslation:)

Imagine a channel running through the anchor point, its direction defined by the axis of translation (a CGVector). Then an item is attached to a rod whose other end slots into that channel and is free to slide up and down it, but whose angle relative to the channel is fixed by its initial definition (given the item’s position, the anchor’s position, and the channel axis) and cannot change.

The channel is infinite by default, but you can add end caps that define the limits of sliding. To do so, you specify the attachment’s attachmentRange; this is a UIFloatRange, which has a minimum and a maximum. The anchor point is 0, and you are defining the minimum and maximum with respect to that; thus, a float range (-100.0,100.0) provides freedom of movement up to 100 points away from the initial anchor point. It may take some experimentation to discover whether the end cap along a given direction of the channel is the minimum or the maximum.

If there is one item, the anchor is fixed. If there are two items, they can slide independently, and the anchor is free to follow along if one of the items pulls it.

Here’s an example of a sliding attachment. We start with a black square and a red square, sitting on the same horizontal, and attached to an anchor midway between them:

// first view
let v = UIView(frame:CGRect(0,0,50,50))
v.backgroundColor = .black
self.view.addSubview(v)
// second view
let v2 = UIView(frame:CGRect(200,0,50,50))
v2.backgroundColor = .red
self.view.addSubview(v2)
// sliding attachment
let a = UIAttachmentBehavior.slidingAttachment(with:v,
    attachedTo: v2, attachmentAnchor: CGPoint(125,25),
    axisOfTranslation: CGVector(0,1))
a.attachmentRange = UIFloatRange(minimum: -200, maximum: 200)
self.anim.addBehavior(a)

The axis through the anchor point is vertical, and we have permitted a maximum of 200. We now apply a slight vertical downward push to the black square:

let p = UIPushBehavior(items: [v], mode: .continuous)
p.pushDirection = CGVector(0,0.05)
self.anim.addBehavior(p)

The black square moves slowly vertically downward, with its rod sliding down the channel, until its rod hits the maximum end cap at 200. At that point, the anchor breaks free and begins to move, dragging the red square with it, the two of them continuing downward and slowly rotating round their connection of two rods and the channel (Figure 4-8).

pios 1709
Figure 4-8. A sliding attachment

Motion Effects

A view can respond in real time to the way the user tilts the device. Typically, the view’s response will be to shift its position slightly. This is used in various parts of the interface, to give a sense of the interface’s being layered (parallax). When an alert is present, for example, if the user tilts the device, the alert shifts its position; the effect is subtle, but sufficient to suggest subconsciously that the alert is floating slightly in front of everything else on the screen.

Your own views can behave in the same way. A view will respond to shifts in the position of the device if it has one or more motion effects (UIMotionEffect), provided the user has not turned off motion effects in the device’s Accessibility settings. A view’s motion effects are managed with methods addMotionEffect(_:) and removeMotionEffect(_:), and the motionEffects property.

The UIMotionEffect class is abstract. The chief subclass provided is UIInterpolatingMotionEffect. Every UIInterpolatingMotionEffect has a single key path, which uses key–value coding to specify the property of its view that it affects. It also has a type, specifying which axis of the device’s tilting (horizontal tilt or vertical tilt) is to affect this property. Finally, it has a maximum and minimum relative value, the furthest distance that the affected property of the view is to be permitted to wander from its actual value as the user tilts the device.

Related motion effects should be combined into a UIMotionEffectGroup (a UIMotionEffect subclass), and the group added to the view. So, for example:

let m1 = UIInterpolatingMotionEffect(
    keyPath:"center.x", type:.tiltAlongHorizontalAxis)
m1.maximumRelativeValue = 10.0
m1.minimumRelativeValue = -10.0
let m2 = UIInterpolatingMotionEffect(
    keyPath:"center.y", type:.tiltAlongVerticalAxis)
m2.maximumRelativeValue = 10.0
m2.minimumRelativeValue = -10.0
let g = UIMotionEffectGroup()
g.motionEffects = [m1,m2]
v.addMotionEffect(g)

You can write your own UIMotionEffect subclass by implementing a single method, keyPathsAndRelativeValues(forViewerOffset:), but this will rarely be necessary.

Animation and Layout

As I’ve already explained, layout ultimately takes place at the end of a CATransaction, when layoutSubviews is sent down the view hierarchy and autolayout constraints are obeyed. It turns out that the layout performed at this moment can be animated simply by animating a call to layoutIfNeeded beforehand. For example:

UIView.animate(withDuration: 0.5) {
    self.layoutIfNeeded()
}

That code means: when layout is performed at the end of this transaction, all changes in the size or position of views should be made, not instantly, but over a period of half a second.

To illustrate, let’s return to the example of layout-driven UI that I gave earlier (“Layout-Driven UI”). Recall that a user taps on a card and the card is moved into the “hand” area by our implementation of layoutSubviews. At the moment, it moves by jumping. Instead, let’s say we’d like it to move smoothly with animation. We can make that happen simply by appending a call for animation of layout when we respond to a card being tapped:

@objc func tapped(_ n:Notification) {
    guard let v = n.object as? Card else {return}
    if let ix = self.hand.firstIndex(of:v) {
        // do nothing
    } else {
        self.hand.append(v)
    }
    // and also please animate layout
    UIView.animate(withDuration: 0.5) {
        self.layoutIfNeeded()
    }
}

Animating layout can be useful also when you’re trying to mediate between animation and autolayout. You may not have thought of these two things as needing mediation, but they do: they are, in fact, diametrically opposed to one another. As part of an animation, you may be changing a view’s frame (or bounds, or center). You’re really not supposed to do that when you’re using autolayout. If you do, an animation may not work correctly. Or, it may appear to work perfectly, because no layout has happened; however, it is entirely possible that layout will happen, and that it will be accompanied by undesirable effects.

The reason, as I explained in Chapter 1, is that when layout takes place under autolayout, what matters are a view’s constraints. If the constraints affecting a view don’t resolve to the size and position that the view has at the moment of layout, the view will jump as the constraints are obeyed. That is almost certainly not what you want.

To persuade yourself that this can be a problem, just animate a view’s position and then ask for immediate layout, like this:

UIView.animateWithDuration(1, animations:{
    self.v.center.x += 100
    }, completion: { _ in
        self.v.superview!.setNeedsLayout()
        self.v.superview!.layoutIfNeeded()
})

If we’re using autolayout, the view slides to the right and then jumps back to the left. This is bad. It’s up to us to keep the constraints synchronized with the reality, so that when layout comes along in the natural course of things, our views don’t jump into undesirable states.

One option is to revise the violated constraints to match the new reality. If we’ve planned far ahead, we may have armed ourselves in advance with a reference to those constraints; in that case, our code can now remove and replace them — or, if the only thing that needs changing is the constant value of a constraint, we can change that value in place. Otherwise, discovering what constraints are now violated, and getting a reference to them, is not at all easy.

But there’s a better way. Instead of performing the animation first and then revising the constraints, we can change the constraints first and then animate layout. (Again, this assumes that we have a reference to the constraints in question.) For example, if we are animating a view (self.v) 100 points rightward, and if we have a reference (con) to the constraint whose constant positions that view horizontally, we would say:

con.constant += 100
UIView.animate(withDuration:1) {
    self.v.superview!.layoutIfNeeded()
}

This technique is not limited to a simple change of constant. You can overhaul the constraints quite dramatically and still animate the resulting change of layout. In this example, I animate a view (self.v) from one side of its superview (self.view) to the other by removing its leading constraint and replacing it with a trailing constraint:

let c = self.oldConstraint.constant
NSLayoutConstraint.deactivate([self.oldConstraint])
let newConstraint = v.trailingAnchor.constraint(
    equalTo:self.view.layoutMarginsGuide.trailingAnchor, constant:-c)
NSLayoutConstraint.activate([newConstraint])
UIView.animate(withDuration:0.4) {
    self.v.superview!.layoutIfNeeded()
}

Another possibility is to use a snapshot of the original view (Chapter 1). Add the snapshot temporarily to the interface — without using autolayout, and perhaps hiding the original view — and animate the snapshot:

let snap = self.v.snapshotView(afterScreenUpdates:false)!
snap.frame = self.v.frame
self.v.superview!.addSubview(snap)
self.v.isHidden = true
UIView.animate(withDuration:1) {
    snap.center.x += 100
}

That works because the snapshot view is not under the influence of autolayout, so it stays where we put it even if layout takes place. If, however, we need to remove the snapshot view and reveal the real view, then the real view’s constraints will probably still have to be revised.

Yet another approach is to animate the view’s transform instead of the view itself:

UIView.animate(withDuration:1) {
    self.v.transform = CGAffineTransform(translationX: 100, y: 0)
}

That’s extremely robust, but of course it works only if the animation can be expressed as a transform, and it leaves open the question of how long we want a transformed view to remain lying around in our interface.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.93.73