SwiftUI Animation
Table of Contents
SwiftUI is a declarative UI framework. That's not only limited to how you position them, but also how you animate them. Animation is an essential part of UI these days. In this article, we will see how easy it is to animate SwiftUI view.
Let's start with an example of how we animate view In UIKit. In this article, we will play around with simple animation, an arrow button that rotates whenever users tap it.
class ViewController: UIViewController {
var showDetail = false
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let conf = UIImage.SymbolConfiguration(font: .systemFont(ofSize: 50))
let image = UIImage(systemName: "chevron.right.circle", withConfiguration: conf)
let button = UIButton(type: .system)
button.translatesAutoresizingMaskIntoConstraints = false
button.setImage(image, for: .normal)
button.addTarget(self, action: #selector(didTapButton(sender:)), for: .touchUpInside)
view.addSubview(button)
view.addConstraints([
view.centerXAnchor.constraint(equalTo: button.centerXAnchor),
view.centerYAnchor.constraint(equalTo: button.centerYAnchor)
])
}
@objc func didTapButton(sender: UIButton) {
showDetail.toggle()
UIView.animate(withDuration: 0.3) {
if self.showDetail {
let radian = 90 * CGFloat.pi / 180
sender.transform = CGAffineTransform(rotationAngle: radian)
} else {
sender.transform = CGAffineTransform.identity
}
}
}
}
If you write the above code in SwiftUI, you can reduce the line of code by more than a half.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
self.showDetail.toggle()
}) {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
}
}
}
The above example has no animation yet. The default animation for state changes is fade in and out. There are many ways to make SwiftUI animate. We would go through all of them one by one.
Add Animations to Individual Views
To make view animate, you apply animation(_:)
[1] modifier to a view. The animation applies to all child views within the view that applied for animation.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
self.showDetail.toggle()
}) {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
.animation(.spring())
}
}
}
Right now, the rotation effect is animate with sprint animation. animation(_:)
modifier applies to all animatable changes within the views it wraps. Let's try to add one more animatable change by scale up the image.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
self.showDetail.toggle()
}) {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
.animation(.spring())
}
}
}
Multiple animations
You can apply multiple animations if you want different animation for each change. The following example would turn off rotation animation by applying nil
animation to the rotation effect.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
self.showDetail.toggle()
}) {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
.animation(nil)
.scaleEffect(showDetail ? 1.5 : 1)
.animation(.spring())
}
}
}
An animation will apply to all animatable changes up until that point. Define two consecutive animations would result in the closest one take effect.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
self.showDetail.toggle()
}) {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
.animation(nil) // This one will apply to .rotationEffect
.animation(.spring()) // This won't animate .rotationEffect
.scaleEffect(showDetail ? 1.5 : 1)
.animation(.spring())
}
}
}
Timing
In UIKit, you can specify the animation duration and delay. In SwiftUI, you can also do that.
Most SwiftUI animation comes with a duration
parameter with one exception, spring animation. Spring animation design in a way that let us specify spring characteristic, e.g., damping and stiffness, then duration is derived from these characters. This makes spring animation don't have duration
parameter. Luckily, Apple provides another way to indirect adjust duration. The way we can change the duration is by using .speed
[2].
.speed
returns an animation that has its speed multiplied by speed. For example, if you had oneSecondAnimation.speed(0.25)
, it would be at 25% of its normal speed, so you would have an animation that would last 4 seconds. In brief, .speed
with speed less than 1 would make animation slower, more than 1 would make animation faster.
newDuration = currentDuration / speed
There are more instance method to modify Animation
like .delay
and .repeat
which quite straightforward. You can check it here.
You can easily support sarunw.com by checking out this sponsor.
Offline Transcription: Fast, privacy-focus way to transcribe audio, video, and podcast files. No data leaves your Mac.
Animate the Effects of State Changes
Many views might rely on the same state. Instead of apply animations to individual views, you can also apply animations to all views by add animations in places you change your state's value. By wrapping the change of state in withAnimation
function, all views that depend on that state would be animated.
From our example, By wrapping the call to .toggle()
with a call to the withAnimation
function, every change related to that state would be animated.
Remove all .animation
and wrap self.showDetail.toggle()
in withAnimation
function.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
withAnimation {
self.showDetail.toggle()
}
}) {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
}
}
}
You can pass the same kinds of animations to the withAnimation
function that you passed to the animation(_:)
modifier. In the following example, we make use of spring animation.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
withAnimation(.spring()) {
self.showDetail.toggle()
}
}) {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
.scaleEffect(showDetail ? 1.5 : 1)
}
}
}
You can still keep .animation
functions in views, and it will take precedence over withAnimation
. The following code will only animate scale, but not rotation.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
withAnimation(.spring()) {
self.showDetail.toggle()
}
}) {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
.animation(nil)
.scaleEffect(showDetail ? 1.5 : 1)
}
}
}
These are everything you need to know about animate changes in SwiftUI. The last thing you need to know about animation is transition
.
Customize View Transitions
Transition is an animation that uses when view transition on- and offscreen (hidden and show). By default, views transition on- and offscreen by fading in and out. You can customize this transition by using the transition(_:)
modifier.
As an example, we will show Text
view when showDetail
becomes true
.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
withAnimation(.spring()) {
self.showDetail.toggle()
}
}) {
VStack {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
if self.showDetail {
Text("Detail")
}
Spacer()
}
}
}
}
The default transition is fade in and out, which should be good enough for most cases, but you also modify the transition. We will specify a transition Animation
that makes Text
view move from the top edge.
struct CustomView: View {
@Binding var showDetail: Bool
var body: some View {
Button(action: {
withAnimation(.spring()) {
self.showDetail.toggle()
}
}) {
VStack {
Image(systemName: "chevron.right.circle").font(.system(size: 50))
.rotationEffect(.degrees(showDetail ? 90 : 0))
if self.showDetail {
Text("Detail").transition(.move(edge: .top))
}
Spacer()
}
}
}
}
By specify move animation .transition(.move(edge: .top))
, we make the text slide from the top edge. As you can see, the dismiss animation is not smooth as you might be expected (it slide up and hang there for a few seconds before disappear).
My first thought is to add animation to Text
, but it doesn't work. Show/hide animation rely only on .transition
. Luckily SwiftUI has a lot of built-in transition[3] which you can mix and match to meet your needs.
The following code won't make the transition fade in/out.
if self.showDetail {
Text("Detail")
.transition(.move(edge: .top))
.opacity(showDetail ? 1: 0) // This doesn't affect transition animation.
}
Mix and match
You can mix two transitions with .combined
[4]. It will return a new transition that is the result of both transitions being applied.
We want to add fade effect, so we combine .move
with .opacity
.
Text("Detail").transition(
AnyTransition.move(edge: .top).combined(with: .opacity)
)
Asymmetric animation
If you don't want the same animation for show and hide, you can create a new animation with a different show (insertion) and hide (removal) animations using .asymmetric
.
The following example, we create a new animation with .asymmetric
with a slide from top appear animation and scale dismissal animation.
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .top).combined(with: .opacity)
let removal = AnyTransition.scale
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
And use it like other animations.
if self.showDetail {
Text("Detail").transition(.moveAndFade)
}
Conclusion
SwiftUI's animation is very powerful. It can do what we can do in UIKit with less code, but that is not all. All animations in SwiftUI are also interruptible![5] If you have ever try interruptible animation in UIKit, you know how hard it is, this fact alone makes me want to use this in my project.
You can easily support sarunw.com by checking out this sponsor.
Offline Transcription: Fast, privacy-focus way to transcribe audio, video, and podcast files. No data leaves your Mac.
Related Resources
SwiftUI's ViewModifier – Learn a crucial concept in SwiftUI, view modifier, and a guide of how to create your custom modifier.
Animating Views and Transitions – Official SwiftUI's tutorial
https://developer.apple.com/documentation/swiftui/section/animation(_:)/ ↩︎
https://developer.apple.com/documentation/swiftui/animation/3263784-speed ↩︎
https://developer.apple.com/documentation/swiftui/anytransition ↩︎
https://developer.apple.com/documentation/swiftui/anytransition/3076194-combined ↩︎
Tap mid-animation would reverse the animation state. ↩︎
Read more article about SwiftUI or see all available topic
Enjoy the read?
If you enjoy this article, you can subscribe to the weekly newsletter.
Every Friday, you'll get a quick recap of all articles and tips posted on this site. No strings attached. Unsubscribe anytime.
Feel free to follow me on Twitter and ask your questions related to this post. Thanks for reading and see you next time.
If you enjoy my writing, please check out my Patreon https://www.patreon.com/sarunw and become my supporter. Sharing the article is also greatly appreciated.
Become a patron Buy me a coffee Tweet ShareSign in with Apple Tutorial, Part 1: Apps
Part 1 in a series Sign in with Apple. In the first part, we will focus on the app part. What we need to do to add Sign in with Apple option in our app.
Sign in with Apple Tutorial, Part 2: Private Email Relay Service
Part 2 in a series Sign in with Apple. In this part, we will talk about the anonymous email address. How to make it work and its limitation.