SwiftUI Animation
Tags: ios, swiftui, animation
Published: 2019-12-26 07:04:16

---
title: SwiftUI Animation
published: true
date: 2019-12-18 00:00:00 UTC
tags: iOS, SwiftUI, Animation
canonical_url: https://sarunw.com/posts/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.

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

``` swift
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))
        }
    }
}
```

![No animation](https://sarunw.com/images/animation-non.gif)<figcaption>SwiftUI changes with no animation</figcaption>

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(_:)`<sup><a href="#fn1" id="fnref1">[1]</a></sup> modifier to a view. The animation applies to all child views within the view that applied for animation.

``` swift
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](https://sarunw.com/images/animation-spring.gif)<figcaption>Spring animation</figcaption>

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.

``` swift
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](https://sarunw.com/images/animation-scale.gif)<figcaption>Rotate and scale animation</figcaption>

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

``` swift
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](https://sarunw.com/images/animation-nil.gif)<figcaption>No animation on rotation effect, while scale effect still change with spring animation</figcaption>

An animation will apply to all animatable changes up until that point. Define two consecutive animations would result in the closest one take effect.

``` swift
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`<sup><a href="#fn2" id="fnref2">[2]</a></sup>.

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

``` swift
newDuration = currentDuration / speed
```

<aside>
<p><code>.speed</code> is an instance method of <code>Animation</code>, it can apply to every animation, not limited to spring.</p>
</aside>

There are more instance method to modify `Animation` like `.delay` and `.repeat` which quite straightforward. You can check it [here](https://developer.apple.com/documentation/swiftui/animation).

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

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

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

``` swift
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)
        }
    }
}
```

<aside>
<p>I don't put the gif here since it looks quite the same as animate a view.</p>
</aside>

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

``` swift
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](https://sarunw.com/images/animation-default-transition.gif)<figcaption>Default transition is fading in and out</figcaption>

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.

``` swift
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](https://sarunw.com/images/animation-edge-top.gif)<figcaption>Move from top edge transition</figcaption>

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<sup><a href="#fn3" id="fnref3">[3]</a></sup> which you can mix and match to meet your needs.

The following code won't make the transition fade in/out.

``` swift
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`<sup><a href="#fn4" id="fnref4">[4]</a></sup>. 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`.

``` swift
Text("Detail").transition(
    AnyTransition.move(edge: .top).combined(with: .opacity)
)
```

![Combination of move and opacity transition](https://sarunw.com/images/animation-edge-top-with-fade.gif)<figcaption>Combination of move and opacity transition</figcaption>

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

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

``` swift
if self.showDetail {
    Text("Detail").transition(.moveAndFade)
}
```

![Asymmetric transition](https://sarunw.com/images/animation-asym.gif)<figcaption>Asymmetric transition</figcaption>

## 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!<sup><a href="#fn5" id="fnref5">[5]</a></sup> 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](https://sarunw.com/images/animation-interupt.gif)<figcaption>All SwiftUI's animations are interruptible</figcaption>

---

*Originally published at https://sarunw.com/posts/swiftui-animation/.*
*Find out my latest article and more at http://sarunw.com I post articles weekly there.*

[![Subscribe](https://thepracticaldev.s3.amazonaws.com/i/6xezzs7o4tcl4d5us96g.jpg)](https://sarunw.us1.list-manage.com/subscribe/post?u=da8ac0f3b786879a00c413a46&amp;id=43463b63ef&SIGNUP=Devto)


## Related Resources #

[SwiftUI's ViewModifier](https://sarunw.com/posts/swiftui-viewmodifier/) – Learn a crucial concept in SwiftUI, view modifier, and a guide of how to create your custom modifier.  
[Animating Views and Transitions](https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions) – Official SwiftUI's tutorial

* * *
<section>
<ol>
<li id="fn1">
<p><a href="https://developer.apple.com/documentation/swiftui/view/3278508-animation" target="_blank" rel="nofollow">https://developer.apple.com/documentation/swiftui/view/3278508-animation</a> <a href="#fnref1">↩︎</a></p>
</li>
<li id="fn2">
<p><a href="https://developer.apple.com/documentation/swiftui/animation/3263784-speed" target="_blank" rel="nofollow">https://developer.apple.com/documentation/swiftui/animation/3263784-speed</a> <a href="#fnref2">↩︎</a></p>
</li>
<li id="fn3">
<p><a href="https://developer.apple.com/documentation/swiftui/anytransition" target="_blank" rel="nofollow">https://developer.apple.com/documentation/swiftui/anytransition</a> <a href="#fnref3">↩︎</a></p>
</li>
<li id="fn4">
<p><a href="https://developer.apple.com/documentation/swiftui/anytransition/3076194-combined" target="_blank" rel="nofollow">https://developer.apple.com/documentation/swiftui/anytransition/3076194-combined</a> <a href="#fnref4">↩︎</a></p>
</li>
<li id="fn5">
<p>Tap mid-animation would reverse the animation state. <a href="#fnref5">↩︎</a></p>
</li>
</ol>
</section>