SwiftUI Animation

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

}

}

}

SwiftUI changes with no animation

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

}

}

}

Spring animation

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



}

}

}

Rotate and scale animation

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



}

}

}

No animation on rotation effect, while scale effect still change with spring animation

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 )

. animation ( . spring ( ) )

. scaleEffect ( showDetail ? 1.5 : 1 )

. animation ( . spring ( ) )

}

}

}

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 .

.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

.speed is an instance method of Animation , it can apply to every animation, not limited to spring.

There are more instance method to modify Animation like .delay and .repeat which quite straightforward. You can check it here.

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 )

}

}

}

I don't put the gif here since it looks quite the same as animate a view.

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

}





}

}

}

Default transition is fading in and out

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

}





}

}

}

Move from top edge transition

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

}

Mix and match #

You can mix two transitions with .combined . 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 )

)

Combination of move and opacity transition

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 )

}

Asymmetric transition

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

All SwiftUI's animations are interruptible

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

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

Tweet

Share

← Home