Chapter 4. Animation

This chapter has been revised for Early Release. It reflects iOS 14, Xcode 12, and Swift 5.3. But screenshots have not been retaken; they still show the Xcode 11 / iOS 13 interface.

Animation is an attribute changing over time. This will typically 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 blending 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.

One of my first iOS 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 is 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

Here’s an interesting fact about how iOS draws to the screen: drawing doesn’t actually take place at the time you give your drawing commands (Chapter 2). 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 periodic, rapid, more or less regular screen refreshes — a perpetual 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.

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 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. To explain what’s actually happening, I have to reveal something about layers that I omitted from Chapter 3. It is not a layer itself that is portrayed on the screen; it’s a derived layer called the presentation layer. 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 an attempt to animate or change a property that’s already in the middle of being animated may be incoherent. 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 send your app to the background, or an incoming phone call might arrive while an animation is in-flight. The system deals neatly 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 the final state that you arranged beforehand. But if you wanted your app to resume an animation where it left off when it was interrupted, that would require some canny coding on your part.

Image View and Image Animation

UIImageView (Chapter 2) 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 a 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).

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. 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 provides 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, transform3D, and, if the view doesn’t implement draw(_:), its backgroundColor (Chapter 1). You can also animate a UIView’s change of contents. In addition, the UIVisualEffectView effect property is animatable between nil and a UIBlurEffect (Chapter 2); and, starting in iOS 11, a view’s underlying layer’s cornerRadius (Chapter 3) 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:

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:

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, it is deprecated and you’re unlikely to use it; block-based animation completely supersedes it. 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. For the bulk of basic view animations, however, the property animator brings some valuable advantages — a full range of timing curves, multiple completion functions, and the ability to pause, resume, and reverse the animation, and to interact by touch with the animated view.

Property Animator Basics

The UIViewPropertyAnimator class adopts the UIViewImplicitlyAnimating protocol, which itself adopts the UIViewAnimating protocol:

UIViewAnimating protocol

A UIViewAnimating adopter can have its animation started, paused, and stopped:

  • startAnimation

  • pauseAnimation

  • stopAnimation(_:)

  • finishAnimation(at:)

Its state property reflects its current animation state (UIViewAnimatingState):

  • .inactive

  • .active

  • .stopped

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

Determines whether the animation is running forward or backward.

UIViewImplicitlyAnimating protocol

A UIViewImplicitlyAnimating adopter can be given animations functions:

  • addAnimations(_:)

  • addAnimations(_:delayFactor:)

It can be given completion functions:

  • addCompletion(_:)

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 discuss those later, when I talk about timing curves. It has some read-only properties describing how it was configured and started (such as its animation’s duration), along with 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 midflight, allow the user to manipulate the animation gesturally, resume the animation, reverse the animation, and much more. I’ll illustrate those features in this and subsequent chapters.

In the simplest case, 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.

When a property animator’s animation is started, it transitions through state changes:

  1. The animator starts life in the .inactive state with isRunning set to false.

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

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

The “animation movie” starts running at the next redraw moment. Once the animation is set in motion, it continues to its finish and then passes 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 false (paused).

  3. The animator then immediately transitions back to the .inactive state with isRunning set to false.

If you have set the animator’s pausesOnCompletion to true, the final step is omitted; the animation pauses, without transitioning back to the .inactive state. Ultimately returning the animator to .inactive is then left up to you. To do that, you first send the animator the stopAnimation(_:) message, causing the animator to enter the special .stopped state. What happens next depends on the parameter you passed to stopAnimation(_:); it’s a Bool:

stopAnimation(_:) parameter is false

You will ultimately call finishAnimation(at:), after which the animator returns to .inactive.

stopAnimation(_:) parameter is true

You want to dispense with finishAnimation(at:) and let the runtime clean up for you. The runtime will bring the animator back to .inactive immediately, without running any completion handlers.

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 eventually bring it back to the .inactive state in good order before the animator goes out of existence.

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.

View Animation Basics

The most important elements of view animation are the animations function and the completion function:

Animations function

Any change to an animatable view property made within an animations function will be animated.

Completion function

The completion function lets us specify what should happen after the animation ends.

More than one animatable view property can be animated at the same time. Here, we animate simultaneous changes both in a 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()

More than one view can be animated at the same time. Suppose we want to replace a view in the view hierarchy with another view, not suddenly, but making the first view dissolve into the second. We start by placing the second view into 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. Finally, in the completion function, we remove the first view 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.

Preventing animation

Code that isn’t about animatable view properties can appear in an animations function with no problem. 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()

Conflicts and additive animations

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 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.

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()

Amazingly, there’s no conflict; instead, we get 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.

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, you configure the animator 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; 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 effectively complete at this point.

Animations function

The animations function contains the commands that set animatable view properties:

  • With a block-based UIView class method, you supply the animations function as the animations: parameter.

  • With a property animator, the animations function is usually provided as the animations: argument when the property animator is instantiated. But you can add an animations function to a property animator after instantiating it; indeed, the init(duration:timingParameters:) initializer requires that you do this, as it lacks an animations: parameter. And you can do that more than once:

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

Completion function

A completion function contains commands that are to be executed when the animation finishes:

  • With a UIView class method, you supply the completion function as 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(_:). 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"

A property animator’s 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 at that time:

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

  • If you called stopAnimation(true), the animator’s completion functions are not executed at all.

Animation duration

The duration of an animation represents how long it takes (in seconds) to run from start to finish:

  • With a block-based UIView class method, the animation duration is the duration: parameter.

  • With a property animator, the animation duration is the duration: parameter when the property animator is initialized.

You can also think of the duration 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.

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, the delay is the delay: parameter.

  • To apply a delay to an animation with a property animator, call startAnimation(afterDelay:) instead of startAnimation.

Animation timing

Specifying a change in a value and a time over which it should be changed is insufficient to describe what should happen. Should we change at a constant rate the whole time? Should we change slowly at first and more quickly later? Questions like these are answered by timing curves. An animation’s timing curve maps interpolated values to time:

  • 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).

  • 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.

.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) {
    self.v.center.x += 100
}

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) {
    self.v.center.x += 100
} completion: { _ in
    self.v.center.x = xorig // *
}

There seems to be a major hole in the design of the block-based animation API; if you say .repeat, you must repeat indefinitely. What if your goal is to repeat a finite number of times? In the past, the solution was 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) {
    UIView.setAnimationRepeatCount(3) // *
    self.v.center.x += 100
} completion: { _ in
    self.v.center.x = xorig
}

Starting in iOS 13, the solution is to omit .autoreverse and .repeat from the animation options, and instead call another UIView class method, modifyAnimations(withRepeatCount:autoreverses:), inside the animations function, containing the actual animations:

let xorig = self.v.center.x
UIView.animate(withDuration: 1) {
    UIView.modifyAnimations(withRepeatCount: 3, autoreverses: true) {
        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

Suppose this animation animates a property already being animated by an animation that is previously ordered or in-flight. Starting this animation might cancel the previous animation, completing the requested change instantly. But with .beginFromCurrentState, 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.

Timing Curves

A timing curve describes how an animation’s speed should vary during the course of the animation. It does this by mapping 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 a cubic timing curve is expressed.

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 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:

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.

Defining a custom cubic timing curve is not difficult. 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 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

The default is zero, and you’ll usually leave it there. A nonzero initial velocity 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. Higher values cause greater overshoot, depending on the damping ratio. 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.

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, as when 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. 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. To illustrate why, I’ll start by showing what you have to do to cancel a block-based animation.

Canceling a block-based animation

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.animate(withDuration: 4) {
    self.v.center = self.pFinal
}

We have a button that we can tap during that animation, and 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, 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, the completion function parameter is .end.

  • If we reversed the animation and it finished by proceeding back to its start, as in our second cancellation example, the parameter is .start.

  • If we called finishAnimation(at:), the parameter is the at: argument we specified in the call.

Canceling a repeating animation

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) {
    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) {
    self.v.center = self.pOrig
}

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, 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 the user, this doesn’t look like it involves any animation; it looks like the view just obeys the slider. But in fact a frozen animation is the way accomplish it.

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, 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

By default, as I explained earlier, only a few basic view properties are animatable through view animation. Changing some other view property in an animations function won’t animate anything. But you can define a custom view property that can be animated in an animations function, provided the custom view property itself changes an animatable view property.

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. So 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.)

To illustrate, I’ll oscillate a view back and forth horizontally while moving it down the screen vertically, forming a zig-zag (Figure 4-2):

pios 1701bbbbb
Figure 4-2. A zig-zag animation
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) {
    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 seems 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:

UIView.animateKeyframes(withDuration: 4, delay: 0) {
    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 legal and meaningful, although the documentation fails to make this clear, to supply a timing curve as part of the options: argument. If you don’t do that, the default is .curveEaseInOut, which may not be what you want. Unfortunately Swift’s obsessive-compulsive attitude toward data types resists folding a UIView.AnimationOptions timing curve directly into a value that is typed as a UIView.KeyframeAnimationOptions; so you have to trick the compiler into letting you do it. 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:

let anim = UIViewPropertyAnimator(
    duration: 4, timingParameters: UICubicTimingParameters())
anim.addAnimations {
    UIView.animateKeyframes(withDuration: 0, delay: 0) {
        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()

In that example, our view animates slowly to the right, and changes color suddenly in the middle of its movement. As the example demonstrates, keyframes do not have to be sequential, nor do they all have to involve the same property. They can be used to coordinate different animations; they are a good way to overlap animations, as well as to chain animations. These aspects of the power and utility of keyframe animations often go unappreciated by beginners. There are other ways to arrange the same outward effect, but this way, the entire animation is placed under the control of a single property animator, and is 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) {
    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) {
    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()
let opts : UIView.AnimationOptions = .transitionFlipFromLeft
UIView.transition(from: self.lab, to: lab2, duration: 0.8, options: opts) {
    _ 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 another handy but probably underutilized iOS animation feature. Earlier, 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 for implementing a dissolve, but calling transition(from:to:...) with a .transitionCrossDissolve animation is simpler and does the same thing.

Implicit Layer Animation

All animation is ultimately layer animation. Up to now, we’ve been talking about view animation, which uses layer animation under the hood. Now we’re going to talk about how to animate a layer directly.

Amazingly, animating a layer can be as simple as setting a layer property. A change in what the documentation calls an animatable property of a CALayer 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.

In Chapter 3 we constructed a compass out of layers. Suppose we have created that interface, and that we have a reference to the arrow layer (arrow). If we rotate the arrow layer by changing its transform property, the arrow rotation is animated:

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

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:

View’s underlying layer

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).

During layer tree preparation

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.

Animatable Layer Properties

CALayer properties listed in the documentation as animatable 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.

Animating a Custom Layer Subclass

Suppose you create a custom CALayer subclass that has custom properties and a custom initializer. Then if you animate any property of that layer, it is possible that you’ll crash with this message in the Xcode console: “Fatal error: Use of unimplemented initializer init(layer:).”

The reasons for this crash have to do with how layer animation works behind the scenes. The runtime has to create a presentation layer that will portray the animation of your custom layer on the screen. And in order to do that, the runtime will call this layer’s init(layer:). So you have to implement init(layer:), to help the runtime create the presentation layer.

The parameter to init(layer:) is the existing instance of your layer subclass, typed as Any. Your implementation should set all custom properties of the new instance, based on their values in the old instance. Typically what you’ll do is cast the parameter to your layer subclass and copy the old values directly into the new instance:

class MyLayer: CALayer {
    var prop: Int
    override init() {
        self.prop = 0
        super.init()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    override init(layer: Any) {
        guard let oldLayer = layer as? MyLayer else {
            fatalError("init(layer:) called with wrong layer type")
        }
        self.prop = oldLayer.prop
        super.init(layer: layer)
    }
}

That’s a minimal, almost trivial implementation of init(layer:) — but without it, animating any property of a MyLayer instance will crash.

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.

flush()

Pauses subsequent code until the current transaction has finished.

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. 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. It’s a way to be notified when any and all animations come to an end.

The flush method can solve the problem of implicit animation not working during preparation of the layer tree. This attempt to add a layer and make it appear by growing from a point doesn’t animate:

// ... create and configure lay ...
lay.bounds.size = .zero
self.view.layer.addSublayer(lay)
CATransaction.setAnimationDuration(2)
lay.bounds.size = CGSize(100,100) // no animation

But this does animate:

// ... create and configure lay ...
lay.bounds.size = .zero
self.view.layer.addSublayer(lay)
CATransaction.flush() // *
CATransaction.setAnimationDuration(2)
lay.bounds.size = CGSize(100,100) // animation

A better alternative, perhaps, would be to use explicit layer animation (discussed in the next section) rather than implicit layer animation.

And now for a revelation. The “redraw moment” that I’ve spoken of earlier is actually the end of the current transaction:

  • 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 both merely convenient façades for Core Animation. Core Animation is explicit layer animation.

Core Animation is vastly more powerful than simple implicit layer animation. Also, it works on a view’s underlying layer, so it’s the only way to apply full-on layer property animation to a view, letting you 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; that 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 (as I know all too well 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. An animation with a duration of 8 and a time offset of 4 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.)

But 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. 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, and the copy added to the layer is immutable, so the animation must be configured beforehand.

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. 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 — and so 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. We can also omit disabling implicit animations, perhaps because the explicit animation of the transform cancels the implicit animation for us. Here’s the condensed version:

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)

There’s no need to change the layer if it doesn’t change as a result of the animation. 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 avoid calculating the new rotation values based on the arrow’s current transform by setting our animation’s isAdditive property to true; the animation’s property values are then added to the existing property value for us (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 them 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. Apple advises against this, though, 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.

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:

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 (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. 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, the rectangle’s border 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.

A custom animatable CALayer property is a solution to a much more general problem. Consider layer animation abstractly as a way of getting the runtime to calculate and send us a series of timed interpolated values. Those values arrive through the runtime repeatedly calling our draw(in:) implementation, but no law says that we have to draw in that implementation: we are free to use those values however we like. In fact, we don’t even have to use the values! We can implement draw(in:) purely as a way to get an event on every frame of an animation, responding to that event in any way we like.

Grouped Animations

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

A CAAnimationGroup 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. The children inherit default property values from their parent. If you don’t set a child’s duration explicitly, for instance, 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-3). 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-3. A boat and the course she’ll sail

Here’s the first animation, the movement of the boat (its position) along its curving path. It illustrates the use of a CAKeyframeAnimation with a CGPath; once we’ve calculated the path, we know the final position of the boat, and we set it so the boat won’t jump back to the start afterward. 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))
}
CATransaction.setDisableActions(true)
self.v.layer.position = next
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:

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

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. If a parent and child have the same duration, but the child’s speed is 1.5, the child’s 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 using Core Animation at the level of the animation or of the layer, with an effect similar to what we did with a property animator earlier.

This feature depends upon the fact that the CAMediaTiming protocol is adopted by CALayer. CAMediaTiming properties of a layer affect the behavior of any animation attached to that layer. The speed property effectively changes the animation’s duration: a speed of 2 means that a 10-second animation plays in 5 seconds, and a speed of 0 means the animation is frozen. When the animation is frozen, the timeOffset property dictates what frame of the animation is displayed.

To illustrate, 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 I’ve also set the layer’s speed to 0, so 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. Reverse them in your mind: when you mean the top, say .fromBottom.

Consider first what happens when we perform a layer transition 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)

Typically, the layer that is to be transitioned will be inside a superlayer that has 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-4, 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-4. 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

To understand how CALayer’s add(_:forKey:) 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 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 when an animation with that key is already in the list, the one that is already in the list is removed. So only one animation with a given key can be in the list at a time; I call this the exclusivity rule.

The exclusivity rule explains why ordering an animation can sometimes 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".)

Unlike a dictionary, the animations list will also accept an animation with no key — the key is nil. Animations with a nil key are not subject to the exclusivity rule; there can be more than one animation in the list with no key.

The forKey: parameter in add(_:forKey:) is 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 misleading; I wish they had named this method something like add(_:identifier:).)

Nevertheless, a relationship between the “key” in add(_:forKey:) and the keyPath of a CAPropertyAnimation does exist: 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 value of the forKey: parameter! Therefore you can misuse the forKey: parameter in add(_:forKey:) as a way of specifying what keyPath an animation animates — and implicit layer animation crucially depends on this fact.

Warning

Many Core Animation examples do misuse forKey: in just that way, supplying nil as the animation’s keyPath and specifying the property to be animated as the “key” in add(_:forKey:). This is wrong! Set the animation’s keyPath explicitly.

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 "transition" — and so 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

Many Core Animation examples set isRemovedOnCompletion to false and set the animation’s fillMode to .forwards or .both as a lazy way of preventing a property from jumping back to its initial value when the animation ends. This is wrong! An animation needs to be removed when it is completed; the fillMode is intended for use with a child animation within a grouped animation. To prevent jumping at the end of the animation, set the animated property value to match the final frame of the 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 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, it will stop; but that doesn’t happen until the next redraw moment. If you need an animation to be removed immediately, you might be able to make that happen by wrapping the remove call in an explicit transaction block.

Actions

For the sake of completeness, I will 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. Your code can hook into the action mechanism to change the behavior of implicit animation in interesting ways. Feel free to skip this section if you don’t want to get into the under-the-hood nitty-gritty of implicit animation.

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 happens to be 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. For an animation, receiving the run(forKey:object:arguments:) message is like being told: “Play yourself!”

Recall 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

Now we know what a CAAction is. But what’s the connection between a CALayer and a CAAction? It all starts with the CALayer instance method action(forKey:). The following events cause a layer’s action(forKey:) method to be called:

  • A CALayer property is set, directly or using setValue(_:forKey:). For most built-in properties, the layer’s response is to call action(forKey:), passing along the name of the property as the key. Certain properties get special treatment:

    • 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.

All of that presupposes that CATransaction.disableActions() is false. If CATransaction.setDisableActions(true) has been called, it prevents the action(forKey:) message from being sent, and that’s the end of the story: there can be no implicit animation in this transaction.

Very well, but let’s say that a layer’s action(forKey:) is called. The layer now embarks upon an elaborate search for an action object (a CAAction) to which it can send the run(forKey:object:arguments:) message. This is the action search.

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:

  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; 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!

You can turn off implicit animation just for a particular property. One way would be to return nil from action(forKey:) itself, in a CALayer subclass. Here’s the code for a CALayer subclass that doesn’t have implicit animation for its position property:

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

For more precise control, 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:

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 intervene at 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’t be overridden through CATransaction. 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 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)
    }
}

A MyAction instance might 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). That’s enough information to build a complete animation from the ground up and add it to the layer.

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. (And we don’t actually have to add an animation to the layer; we are free to interpret the setting of this property however we like!)

Instead of modifying a 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:). The delegate can behave differently depending on what key this is (and even what layer this 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 implementation of one of these methods, you’ll need to cast it to CAAction to quiet the compiler; you’re lying (NSNull does not adopt CAAction), 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. We will then 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
    // ...
}

(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 triggered when a layer is added to a superlayer (key "onOrderIn") and when a layer’s sublayers are changed by adding or removing a sublayer (key "sublayers").

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 == "onOrderIn" {
        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 "onOrderOut". 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). To illustrate, let’s implement an arbitrary 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. But 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 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.

Here’s some 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-5).

pios 1705
Figure 4-5. 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.

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.

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.

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-6):

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-6. 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. 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. 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-7):

cell2.beginTime = 1.4
cell2.duration = 0.4
pios 1707
Figure 4-7. 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. Figure 4-8 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-8. Midway through a starburst transition

Animating a Core Image transition filter is up to us. 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 tran = CIFilter.flashTransition()
tran.inputImage = CIImage(color: CIColor(color:.red))
tran.targetImage = moi
tran.center = self.moiextent.center
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 start 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. 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. 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. Even if an animation is already in progress, a behavior’s configuration can be changed and behaviors can be added to and removed from the animator.

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, displaying the planet Mars) 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 have 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, 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 apply a slight rightward impulse 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, even though it displays a round image, is itself 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. The effect is quite realistic: the image itself appears to roll to the right after it bounces. But 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 can receive 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 each behavior 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 function so as 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.

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; 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. 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; 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 = self.view.bounds.center
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. 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. 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 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; 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.

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, if the user tilts the device, the alert shifts its position; the effect is a subtle suggestion 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:

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! To make that happen, order an animation of layoutIfNeeded:

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

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

Animating layout can be useful when you’re trying to mediate between animation and autolayout. You may not have thought of those 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 at first, before any layout has happened, but then there can be undesirable side effects when layout does happen.

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.animate(withDuration: 1) {
    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.) 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. But if 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