Animations in SwiftUI are automatic and magical. You have to define two states of a view and SwiftUI will take care of the rest, animating the changes between two states. It also allows you to animate changes for individual views and transitions between views. The framework comes with a number of built-in animations to create different effects.
In this article we will learn how to animate views using implicit and explicit animations in SwiftUI.
Implicit and Explicit Animations
SwiftUI offers two types of animations, implicit and explicit. Both allow you to animate views and view transitions. To implement implicit animations the framework offers a modifier called animation. You attach this modifier to the views you want to animate and specify the type of animation you prefer. Optionally, you can define the duration and delay of the animation. SwiftUI will then automatically render the animation based on the state changes of the views.
Many of the view types included in SwiftUI contain properties that control the appearance of the view such as scale, opacity, color, or rotation angle. Properties of this type are animatable, in that the change from one property state to another can be animated rather than occurring instantly.
Implicit animations using modifier animation() implement an animation of any of the animatable properties on a view before the modifier animation.
Explicit animations offer more precise control over the animations you want to present. Instead of attaching a modifier to the view, you tell SwiftUI what state changes you want to animate within the withAnimation() block.
Implicit Animations
To understand how we can create an implicit animation, let’s look at an example:
Open XCode and create a new project, selecting Swift as the programming language and SwiftUI as the interface.
In this example we are going to create the following animation that you can see in the before and after images:
It is a simple touchable figure consisting of a MacBook and a circle. When a user touches the MacBook or circle, the circle changes color and the color of the MacBook changes to blue. Also the MacBook increases in size. So we have different states:
-The color of the circle changes from blue to gray.
-The color of the MacBook changes from white to blue.
-The size of the MacBook increases.
To implement this circle using SwiftUI, add the following code to ContentView.swift:
struct ContentView: View {
@State private var circleColorChanged: Bool = false
@State private var macbookColorChanged: Bool = false
@State private var macbookSizeChanged: Bool = false
var body: some View {
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundStyle(circleColorChanged ? Color(.systemGray5) : .blue)
Image(systemName: "macbook")
.foregroundStyle(macbookColorChanged ? .blue : .white)
.font(.system(size: 100))
.scaleEffect(macbookSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
circleColorChanged.toggle()
macbookColorChanged.toggle()
macbookSizeChanged.toggle()
}
}
}
In this Swift code, we define three state variables to represent the states of the circle color, the MacBook color, and the MacBook size, with the initial value set to false.
We use a ZStack to overlay the MacBook image on top of the circle. SwiftUI comes with the onTapGesture modifier to detect user tap gestures, which can be added to make any view tappable.
In the onTapGesture closure, we toggle the state variables to change the appearance of the view.
In the XCode preview you can see how the changes happen:
But these changes are not animated. To animate the changes we use the animation modifier. SwiftUI monitors the value changes specified in the animation modifier. When a value changes, it calculates and renders the animation, allowing the views to seamlessly transition from one state to another. If you tap the MacBook again, you should see a graceful animation. The modifier animation can be applied not only to a single view, but also to a group of views. To create this implicit animation we will add the modifier animation to the ZStack like this, the code would be as follows:
struct ContentView: View {
@State private var circleColorChanged: Bool = false
@State private var macbookColorChanged: Bool = false
@State private var macbookSizeChanged: Bool = false
var body: some View {
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundStyle(circleColorChanged ? Color(.systemGray5) : .blue)
Image(systemName: "macbook")
.foregroundStyle(macbookColorChanged ? .blue : .white)
.font(.system(size: 100))
.scaleEffect(macbookSizeChanged ? 1.0 : 0.5)
}
.animation(.default, value: circleColorChanged)
.animation(.default, value: macbookSizeChanged)
.onTapGesture {
circleColorChanged.toggle()
macbookColorChanged.toggle()
macbookSizeChanged.toggle()
}
}
}
In the example, we use the default animation, which is the spring animation on iOS. SwiftUI also offers a variety of built-in animations to choose from, including linear, easeIn, easeOut, and easeInOut animations.
Linear animation animates changes at a constant rate, while easing animations have varying rates and easing curves.
To use an alternative animation, just set the specific animation in the animation modifier. Suppose you want to use the linear animation; you can change .default to the following:
.animation(.linear, value: circleColorChanged)
To customize the spring animation, you can call the spring function and specify the animation parameters, such as duration and bounce type:
animation(.spring(.bouncy, blendDuration: 1.0), value: circleColorChanged)
This generates a spring-based animation that gives the example MacBook a bouncy effect. By adjusting these parameters, different animation effects can be created.
Explicit Animations
This is how views are animated using implicit animation. Let’s see how we can achieve the same result using explicit animation. As explained before, it is necessary to wrap state changes in a withAnimation block. To create the same animated effect, you can write the code like this:
struct ContentView: View {
@State private var circleColorChanged: Bool = false
@State private var macbookColorChanged: Bool = false
@State private var macbookSizeChanged: Bool = false
var body: some View {
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundStyle(circleColorChanged ? Color(.systemGray5) : .blue)
Image(systemName: "macbook")
.foregroundStyle(macbookColorChanged ? .blue : .white)
.font(.system(size: 100))
.scaleEffect(macbookSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
withAnimation(.default){
circleColorChanged.toggle()
macbookColorChanged.toggle()
macbookSizeChanged.toggle()
}
}
}
}
We no longer use the animation modifier; instead, we wrap the code inside onTapGesture with withAnimation. The withAnimation function takes an animation parameter, where we specify the type of animation we want to use.
To use a spring animation you can update the code like this:
.onTapGesture {
withAnimation(.spring(.bouncy, blendDuration: 1.0)){
circleColorChanged.toggle()
macbookColorChanged.toggle()
macbookSizeChanged.toggle()
}
}
With explicit animation, you have fine control over the state changes you want to animate. If you don’t want to animate the heart icon’s resizing, you can exclude that line of code from the withAnimation block:
.onTapGesture {
withAnimation(.spring(.bouncy, blendDuration: 1.0)){
circleColorChanged.toggle()
macbookColorChanged.toggle()
}
macbookSizeChanged.toggle()
}
In this case, SwiftUI excludes the scaling animation and only animates the color changes.
You might be wondering if we can disable the scaling animation using implicit animation. You can! You can reorder the .animation modifier to prevent SwiftUI from animating a certain state change.
By reordering the .animation modifier, you can achieve the desired effect. Here is the modified code:
struct ContentView: View {
@State private var circleColorChanged: Bool = false
@State private var macbookColorChanged: Bool = false
@State private var macbookSizeChanged: Bool = false
var body: some View {
ZStack {
Circle()
.frame(width: 200, height: 200)
.foregroundStyle(circleColorChanged ? Color(.systemGray5) : .blue)
.animation(.spring(.bouncy, blendDuration: 1.0), value: circleColorChanged)
Image(systemName: "macbook")
.foregroundStyle(macbookColorChanged ? .blue : .white)
.font(.system(size: 100))
.animation(.spring(.bouncy, blendDuration: 1.0), value: macbookColorChanged)
.scaleEffect(macbookSizeChanged ? 1.0 : 0.5)
}
.onTapGesture {
circleColorChanged.toggle()
macbookColorChanged.toggle()
macbookSizeChanged.toggle()
}
}
}
For the image view, we place the modifier animation just before scaleEffect. This will disable the animation. SwiftUI will only animate color changes and exclude the scale animation.
While you can achieve the same animation using implicit animation, in my opinion, it is more convenient to use explicit animation in this case.