Animation - codepath/ios_guides GitHub Wiki

Introduction

iOS was built from the ground up to support animation. It is absolutely required to maintain the illusion of a touch-based interface.

This guide covers UIKit's UIView.animate and Core Animation's CABasicAnimation. If you are building a new screen in SwiftUI, animations are driven by state changes via withAnimation and the value-based .animation(_:value:) view modifier — see Apple's SwiftUI animations documentation for the modern API. The UIKit APIs below remain the right choice for animating views inside a UIKit hierarchy, and the SwiftUI Equivalents section at the end maps each UIKit example to its SwiftUI counterpart.

Simple Animation with UIKit

Most of the time, you will perform animation by working directly with UIKit. Say you have a view, myView, and you want to animate its movement from its current position to a new position, the myNewFrame rect. You simply call:

UIView.animate(withDuration: 0.35, animations: {
    self.myView.frame = myNewFrame
})

Behind the scenes, it is making calls to a lower layer in the stack, Core Animation. You work with Core Animation directly when you need to create complex animation, or do something performance intensive.

There are more complex animation methods available to avoid going lower. If you need to call code when the animation completes:

UIView.animate(withDuration: 0.35, animations: {
    self.myView.frame = myNewFrame
}, completion: { finished in
    print("Animation completed.")
})

If you want to pass even more options, such as the animation curve:

UIView.animate(withDuration: 0.35, delay: 0.5, options: .curveEaseInOut, animations: {
    self.myView.frame = myNewFrame
}, completion: { finished in
    print("Animation completed.")
})

This style of animation, where you just specify new attributes and let Core Animation figure it out for you is implicit animation. To create complex animation, you need to delve directly into layers.

Understanding Layers

Core Animation is a misnomer for the framework. Its original name was LayerKit. It is a framework for displaying a hierarchy of layers on screen.

A layer is a container for a bitmap image. Layers are rendered on screen via the GPU, which makes it super fast to render most animation.

UIKit's view hierarchies are backed by a near-identical layer hierarchy. In fact, every UIView has a .layer property for accessing its corresponding layer. Behind the scenes, UIView populates layer's contents for you. So while a UILabel displays text, it is ultimately flattened to a bitmap and handed over to its associated layer.

Views are so tightly associated to layers that many attributes are shared; for instance, if you change the layer's opacity value, then the view's alpha will also change.

The big difference between a view and a layer is a view can receive events. If you don't need the overhead, you can manage your layer's sublayers yourself. This might improve performance if you're dealing with hundreds of layers on screen at once, such as if you're building a particle system. Most of the time, this is overkill.

Implicit Animation in Layers

When layers exist by themselves, not attached to a view, most property changes are animated by default. However, layers attached to views have this default animation turned off. When you call UIView's animate(withDuration:), implicit animation is turned on again.

Transactions

All animation changes are bundled together in a transaction. This is usually invisible, as there's usually one created for you with every pass through the run loop. This lets you make a bunch of changes that are batched together at once in the end.

Imagine you make two changes to your layer:

myLayer.opacity = 0.0
myLayer.opacity = 1.0

The first change has no effect. There is no momentary "blip" of 0.0 opacity. Instead, after all your code is run, Core Animation looks at all the final values to create the animation.

Explicit Animation

An explicit animation allows you to construct more complex animations, such as animating a view along a bezier path.

To construct a basic animation:

myView.layer.position = endPosition
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = NSValue(cgPoint: startPosition)
animation.toValue = NSValue(cgPoint: endPosition)
animation.duration = 1.0
myView.layer.add(animation, forKey: "MyAnimationKey")

The position key is the name of the property you want animated. The forKey argument in the add(_:forKey:) method is a made-up key you will use to identify the animation in flight.

One caveat from the above example is that we made sure to update the underlying layer.position value in addition to applying the animation. Otherwise, the animation would have completed and then the layer would have immediately jumped back to initial position.

SwiftUI Equivalents

SwiftUI has no UIView.animate(withDuration:animations:) — instead you change state and let SwiftUI animate the difference. The UIKit examples above map as follows.

Simple animation. Wrap the state change in withAnimation(_:_:); any view whose layout reads that state animates to its new value:

withAnimation(.easeInOut(duration: 0.35)) {
    isMoved.toggle()
}

Alternatively, attach the value-based animation(_:value:) modifier so the view animates whenever the observed value changes:

Circle()
    .offset(x: isMoved ? 120 : 0)
    .animation(.easeInOut(duration: 0.35), value: isMoved)

Animation curve and delay. The options: .curveEaseInOut / delay: example maps onto the Animation value itself — for example .easeInOut(duration: 0.35).delay(0.5) (see delay(_:)).

Springs. Spring behavior is also part of the Animation value: use spring(response:dampingFraction:blendDuration:), or the newer spring(duration:bounce:blendDuration:) form, where a bounce of 0 is a critically damped spring (no overshoot) and positive values up to 1.0 add bounciness:

withAnimation(.spring(duration: 0.5, bounce: 0.3)) {
    isMoved.toggle()
}

Completion handlers. On iOS 17+, withAnimation(_:completionCriteria:_:completion:) adds a completion closure, mirroring the UIKit completion: example:

withAnimation(.easeInOut(duration: 0.35)) {
    isMoved.toggle()
} completion: {
    print("Animation completed.")
}

Earlier deployment targets have no built-in SwiftUI animation-completion callback; schedule follow-up work separately (for example with DispatchQueue.main.asyncAfter matched to the animation duration).

Explicit animation. SwiftUI does not expose the layer tree, so there is no direct CABasicAnimation equivalent. Multi-step animations use the keyframeAnimator(initialValue:trigger:content:keyframes:) modifier (iOS 17+), and custom animatable properties conform to Animatable.

References