Monday, December 5, 2022
HomeTechnologyTransactions and Animations · objc.io

Transactions and Animations · objc.io


In SwiftUI, there are many different ways to animate something on screen. You can have implicit animations, explicit animations, animated bindings, transactions, and even add animations to things like FetchRequest.

Implicit animations are animations that are defined within the view tree. For example, consider the following code. It animates the color of a circle between red and green:

								struct Sample: View {
    @State var green = false
    var body: some View {
        Circle()
            .fill(green ? Color.green : Color.red)
            .frame(width: 50, height: 50)
            .animation(.default)
            .onTapGesture {
                green.toggle()
            }
    }
}

							

This style of animation is called implicit because any changes to the subtree of the .animation call are implicitly animated. When you run this code as a Mac app, you will see a strange effect: on app launch, the position of the circle is animated as well. This is because the .animation(.default) will animate every time anything changes. We have been avoiding and warning against implicit animations for this reason: once your app becomes large enough, these animations will inevitably happen when you don’t want them to, and cause all kinds of strange effects. Luckily, as of Xcode 13, these kind of implicit animations have been deprecated.

There is a second kind of implicit animation that does work as expected. This animation is restricted to only animate when a specific value changes. In our example above, we only want to animate whenever the green property changes. We can limit our animation by adding a value:

								struct Sample: View {
    @State var green = false
    var body: some View {
        Circle()
            .fill(green ? Color.green : Color.red)
            .frame(width: 50, height: 50)
            .animation(.default, value: green)
            .onTapGesture {
                green.toggle()
            }
    }
}

							

In our experience, these restricted implicit animations work reliably and don’t have any of the strange side-effects that the unbounded implicit animations have.

You can also animate using explicit animations. With explicit animations, you don’t write .animation in your view tree, but instead, you perform your state changes within a withAnimation block:

								struct Sample: View {
    @State var green = false
    var body: some View {
        Circle()
            .fill(green ? Color.green : Color.red)
            .frame(width: 50, height: 50)
            .onTapGesture {
                withAnimation(.default) {
                    green.toggle()
                }
            }
    }
}

							

When using explicit animations, SwiftUI will essentially take a snapshot of the view tree before the state changes, a snapshot after the state changes and animate any changes in between. Explicit animations also have none of the problems that unbounded implicit animations have.

However, sometimes you end up with a mix of implicit and explicit animations. This might raise a lot of questions: when you have both implicit and explicit animations, which take precedence? Can you somehow disable implicit animations when you’re already having an explicit animation? Or can you disable any explicit animations for a specific part of the view tree?

To understand this, we need to understand transactions. In SwiftUI, every state change has an associated transaction. The transaction also carries all the current animation information. For example, when we write an explicit animation like above, what we’re really writing is this:

								withTransaction(Transaction(animation: .default)) {
    green.toggle()
}

							

When the view’s body is reexecuted, this transaction is carried along all through the view tree. The fill will then be animated using the current transaction.

When we’re writing an implicit animation, what we’re really doing is modifying the transaction for the current subtree. In other words, when you write .animation(.easeInOut), you’re modifying the subtree’s transaction.animation to be .easeInOut.

You can verify this with the .transaction modifier, which allows you to print (and modify) the current transaction. If you run the following code, you’ll see that the inner view tree receives a modified transaction:

								Circle()
    .fill(green ? Color.green : Color.red)
    .frame(width: 50, height: 50)
    .transaction { print("inner", $0) }
    .animation(.easeInOut)
    .transaction { print("outer", $0) }

							

This answers our first question: the implicit animation takes precedence. When you have both implicit and explicit animations, the root transaction carries the explicit animation, but for the subtree with the implicit animation, the transaction’s animation is overwritten.

This brings us to our second question: is there a way to disable implicit animations when we’re trying to create an explicit animation? And let me spoil the answer: yes! We can set a flag disablesAnimations to disable any implicit animations:

								struct Sample: View {
    @State var green = false
    var body: some View {
        Circle()
            .fill(green ? Color.green : Color.red)
            .frame(width: 50, height: 50)
            .animation(.easeInOut, value: green)
            .onTapGesture {
                var t = Transaction(animation: .linear(duration: 2))
                t.disablesAnimations = true
                withTransaction(t) {
                    green.toggle()
                }
            }
    }
}

							

When you run the above code, you’ll see that the transaction’s animation takes precedence over the implicit animation. The flag disablesAnimations has a confusing name: it does not actually disable animations: it only disables the implicit animations.

To understand what’s happening, let’s try to reimplement .animation using .transaction. We set the current transaction’s animation to the new animation unless the disablesAnimations flag is set:

								extension View {
    func _animation(_ animation: Animation?) -> some View {
        transaction {
            guard !$0.disablesAnimations else { return }
            $0.animation = animation
        }
    }
}

							

Note: An interesting side-effect of this is that you can also disable any .animation(nil) calls by setting the disablesAnimations property on the transaction. Note that you can also reimplement .animation(_:value:) using the same technique, but it’s a little bit more work as you’ll need to remember the previous value.

Let’s look at our final question: can you somehow disable or override explicit animations for a subtree? The answer is “yes”, but not by using .animation. Instead, we’ll have to modify the current transaction:

								extension View {
    func forceAnimation(animation: Animation?) -> some View {
        transaction { $0.animation = animation }
    }
}

							

For me personally, transactions were always a bit of a mystery. Somebody in our SwiftUI Workshop asked about what happens when you have both implicit and explicit animations, and that’s how I started to look into this. Now that I think I understand them, I believe that transactions are the underlying primitive, and both withAnimation and .animation are built on top of withTransaction and .transaction.

If you’re interested in understanding how SwiftUI works, you should read our book Thinking in SwiftUI, watch our SwiftUI videos on Swift Talk, or even better: attend one of our workshops.

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments