This article is part of the "Pro to SwiftUI" series, mainly sharing some insights, experiences, and pitfalls recorded after developing a few apps using SwiftUI. I hope it can help you.
This article will talk about what’s transaction in SwiftUI and its relation to animation, and also how does it propagate between view updates.
Transaction
The official document about the term transaction is:
The context of the current state-processing update.
And what does it mean? Each word in this sentence means something, and let’s divide and conquer:
It’s a context, which normally stores information of a given circumstance and can be looked up later on.
It’s used during the state-processing update. That’s, if no state is updated then no transaction is used.
It’s used for the current moment, not the past nor the future. It will be discard out of the state-processing update. And it’s a struct, which is a value type and lightweight type in Swift.
To understand the Transaction
, I think we should first focus on how the views in SwiftUI is updated.
How does SwiftUI update views?
SwiftUI provides a declarative and unified way for us to describe the UI based on the current state.
There are two parts of it: the input state and the output views.
It’s like a function of y = f(x)
, where y is the views and the x is the state. If the states are changed, then the views are changed accordingly.
A view’s state come from various way, like the @State
, @StateObject
and @ObservedObject
, which can trigger the view update if the states are changed. When the states changed, the body
compute property of the view will be called, a new value type of View
is constructed and the attribute graph is updated and so the drawing process.
Views along with modifiers will be “translated” as the attribute graph underneath. This deeper introduction of this term will not be talked here, but it may be in a future article.
What’s between state changes?
Consider this simple example below. The view has a state of toggled
and it can be altered when tapping the Rectangle. When the toggled
changed, the fill color of rectangle changes opacity accordingly.
When we first tap the rectangle, the toggled state changes from false to true. The corresponding views changes from the one with 50% red to 100% red. So is the text view.
Without any other information, the system will draw the changed state of UI in the next drawing cycle and that’s it.
But inside this process, there’s an implicit process done by SwiftUI:
When changing the state of the View, it creates a context, which can hold some extra information to use later in the view updates.
The context is passed down to the view hierarchy and let the descendants to copy or change the information they need.
The system depends on the extra information holding by the views and decide how to perform between view changes.
I am hiding the term Transaction
in the process above, in which it’s the context to hold information and being passed through the view hierarchy.
When we change state like this:
It’s equivalent to this:
Since no other information we provide, with or without the explicit transaction, the system will do the same thing: draw the UI in the next drawing cycle.
Provide animation information in a transaction
Drawing a frame of UI is relatively simple but great UI/UX should do more than that: animate between state changes.
You may already know how to animate state changes:
which is equivalent to this:
Now the transaction has the animation information(the timing function and the duration stuff), SwiftUI now can introduce a presentation state of views, which is a intermediate state of UI. In this example above, the rectangle can have some intermediate states with intermediate opacity duration the animation, with the final state of opacity of 100%.
You may notice that the toggled text is also animated, due to the fact that it changes its text content based on the same state, which is the toggled
boolean.
How to remove the animation on the text view only? In fact, we can utilize the transaction propagation mechanism to modify the animation of the text specially.
Transaction Propagation
Transaction is propagated through the view hierarchy during a state update.
Take a look at the sample code below again:
When we change the toggled
state of this view, a transaction with default animation is created, and while generating the attribute graph for the view hierarchy, the transaction is passed from the top of the root view down to the final leaf view. Each view is able to read and modify the transaction values, the modified transaction will be propagated down the view hierarchy to the child view and so on until the leaf view.
To modify the transaction being passed, you use the transaction(_:)
modifier:
The code above:
Set the animation value to nil for the transaction when it’s being propagated
The view’s decedents will get the transaction with nil animation unless its ancestor changes it
So for the following code:
The transaction propagation is depicted below:
First a transaction with default animation is created when the button is clicked
The
toggled
state is changed and thebody
property of this view will be called
The
VStack
receive the transaction and change its animation property to nil. Its decedents like Button with label inside and theHStack
with theText
also receive a transaction with nil animation
The
Image
view use thetransaction(_:)
modifier to set the animation to a bouncy animation
In this view update, only the image has the animation to fire
Transaction vs animation
The code above I have shown some code that the withAnimation
API is equivalent to constructing a Transaction
with animation as parameter.
Basically what we are dealing with Transaction
is the animation property. And yes we can avoid using Transaction
API in most cases and just deal with the animation API. Just remember: animation
is the shortcut API SwiftUI provides for us to use, but underneath is the transaction that does the work.
For example, to alter states with animation, instead of:
You can:
Instead of:
You should:
You should always prefer using animation(_,value:)
to control the animation of a view, which allows you to specify which state change the animation is applying to. If you use the transaction(_:)
to set the animation, it will apply to this view whatever states are changed, which sometimes is what we want.
Also, there is an exception that we can’t replace animation(_,value:)
with transaction. In iOS 17.0 and macOS 14.0, the Widget in the home screen or desktop is interactive and can be animated between different timeline state. However the animation is not backed by the transaction since there is no state-update in Widget. Instead we use animation(_,value:)
to specify the animation of the view and the WidgetKit will do the heavy lifting for us.
In iOS 17.0 and macOS 14.0, the
Transaction
API is allowing us to custom aTransactionKey
and pass value with the transaction. This article won’t talk about this topic though, and if you are interested please refer to the Apple documentation.
Fine control over transactions
The transaction(_:)
method has various overloads, which you can use to fine control the area how the transaction is taking effect.
This article won’t discuss this topic though, and if you are interested in this, please refer to the Explore SwiftUI animation in this year 2023.
https://developer.apple.com/wwdc23/10156
Also, with iOS 17.0 and macOS 14.0, it’s able to use the tracksVelocity
API to track velocity of user gesture and perform smoother animation at the end of the gesture:
This property can be enabled in an interactive context to track velocity during a user interaction so that when the interaction ends, an animation can use the accumulated velocities to create animations that preserve them.
Afterwords
This article talks about what is the Transaction, its relation to the animation API and how it’s propagated through the view hierarchy. To keep this article simple and easy-to-learn I avoid discussing the attribute graph and how the order of transaction view modifier affects the result, which may be covered in the future articles.
If you have any questions please feel free to ask in the comment section.
这篇写的太好了