The Secret to Flawless SwiftUI Animations: A Deep Dive into Transactions

Published on

Get weekly handpicked updates on Swift and SwiftUI!

SwiftUI is widely popular due to its easy-to-use animation API and low animation design threshold. However, as the complexity of applications increases, developers gradually realize that although animation design is very simple, achieving precise and detailed animation control is not easy. At the same time, in the animation system of SwiftUI, there is little explanation about Transaction, and there is no systematic explanation of its operation mechanism in both official materials and third-party articles.

This article will explore the principles, functions, creation, and distribution logic of Transaction, and tell readers how to achieve more accurate animation control in SwiftUI, as well as other issues to pay attention to.

What is Transaction

  • Transaction is a value that contains the context SwiftUI needs to understand when processing current state changes, the most important of which is the animation function used to calculate interpolation.
  • Similar to environment values, SwiftUI implicitly propagates transactions downwards in the view hierarchy.
  • The core difference between “explicit animation” and “implicit animation” is the location and logic of generating and dispatching transactions.
  • Transactions are only related to current state changes. Whenever a state changes, SwiftUI generates new transactions based on whether it is initiated by “explicit animation” or whether there is a declaration of “implicit animation”, and propagates them in the required view hierarchy.
  • Downstream transaction generators (.animation, .transaction) will select whether to use the upstream distributed transaction or generate a new transaction based on the settings.
  • When the state changes, the animatable components associated with the current change state (usually conforming to the Animatable protocol) will obtain the context (transaction) of this state change, obtain the animation curve function, and use it to calculate interpolation.
  • Transactions cannot be generated or dispatched alone, they are accompanying information for state changes.

I believe that many readers will still feel confused after reading the above description of transaction. Therefore, in the following content, we will introduce and elaborate on the details and implementation of transactions in more detail to help you better understand.

How to observe changes in transaction

With the .transaction view modifier, we can create a tool to help us better study and understand transactions.

Swift
extension View {
    @ViewBuilder
    func transactionMonitor(_ title: String, _ showAnimation: Bool = true) -> some View {
        transaction {
            print(title, terminator: showAnimation ? ": " : "\n")
            if showAnimation {
                print($0.animation ?? "nil")
            }
        }
    }
}

What is implicit animation

Implicit animation is a transaction declared on view branches with the .animation or .transaction (usually .animation) modifier, indicating what transaction should be created when the state changes.

SwiftUI will call implicit animation to create a transaction under the following conditions:

  • The current view branch changes when the state changes.
  • Implicit animation is declared on the current view branch.

The following code will show how implicit animation creates a transaction and passes it down:

Swift
struct ImplicitAnimationDemo: View {
    @State private var isActive = false
    var body: some View {
        VStack {
            Text("Hello")
                .font(.largeTitle)
                .offset(x: isActive ? 200 : 0)
                .transactionMonitor("inner")
                .animation(.smooth, value: isActive)
                .transactionMonitor("outer")

            Text("World")
                .transactionMonitor("world")

            Toggle("Active", isOn: $isActive)
                .padding()
        }
        .transactionMonitor("VStack")
        .animation(.linear, value: isActive)
    }
}

https://cdn.fatbobman.com/implicit-animation-demo1_2023-06-25_15.43.04.2023-06-25_15_43_40.gif

Output:

Bash
VStack: BezierAnimation(duration: 0.35, curve: SwiftUI.UnitCurve.CubicSolver(ax: -2.0, bx: 3.0, cx: 0.0, ay: -2.0, by: 3.0, cy: 0.0))
outer: BezierAnimation(duration: 0.35, curve: SwiftUI.UnitCurve.CubicSolver(ax: -2.0, bx: 3.0, cx: 0.0, ay: -2.0, by: 3.0, cy: 0.0))
inner: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
VStack: nil
outer: nil
  • By toggling isActive, the application’s state has changed.
  • SwiftUI found that both the Text("Hello") and the enclosing VStack in the view hierarchy change when the state changes.
  • The VStack declares the transaction to be created when isActive changes using .animation (with a linear animation function).
  • The Text("Hello") declares the transaction to be created when isActive changes using .animation (with a smooth animation function).
  • SwiftUI calls .animation on VStack to create a new transaction and passes it down. The corresponding value can be seen through the output information of VStack and outer.
  • SwiftUI calls .animation on Text("Hello") to create a new transaction and passes it down, replacing the transaction passed down by VStack (check the output information of inner).
  • After the state change is complete, SwiftUI resets the transaction outside VStack and Text("Hello") (to nil).

A few tips:

  • SwiftUI may set transactions (with a value of nil) for some views during the initial stage of the application, which does not affect the views from getting the correct transactions when the state changes.
  • SwiftUI may reset transactions (with a value of nil) for some views after the state changes, which does not affect the next animation (a new transaction will be created the next time the state changes).
  • When the passed-in transaction is nil, SwiftUI optimizes the timing of the .transaction modifier closure. If the transaction is not modified in the closure, it may be ignored (not called).

What is the difference between .animation and .transaction modifiers?

The .animation modifier is a convenient version of the .transaction modifier. Similarly, withAnimation for “explicit animations” is a convenient version of withTransaction.

For example, we can create a version of the .animation modifier that is associated with specific values for iOS 13 using the following code.

Swift
extension View {
    func myAnimation<V>(_ animation: Animation?, value: V) -> some View where V: Equatable {
        modifier(MyAnimationWithValueModifier(animation: animation, value: value))
    }
}

struct MyAnimationWithValueModifier<V>: ViewModifier where V: Equatable {
    @State private var holder: Holder
    private let value: V
    private let animation: Animation?
    init(animation: Animation?, value: V) {
        self.animation = animation
        self.value = value
        _holder = State(wrappedValue: Holder(value: value))
    }

    func body(content: Content) -> some View {
        content
            .transaction { transaction in
                guard value != holder.value else { return }
                holder.value = value
                guard !transaction.disablesAnimations else { return }
                transaction.animation = animation
            }
            .onAppear {} // Fixed the issue where the animation was not playing correctly on its first execution.
    }

    class Holder {
        var value: V
        init(value: V) {
            self.value = value
        }
    }
}

Code Tips:

  • Save the value to be compared.
  • Update the saved value when the associated value changes.
  • Check the disablesAnimations property of the upstream transaction to determine whether to replace the upstream transaction with a new transaction. (More information about disablesAnimations is explained below.)
  • onAppear is used to ensure that the first setting takes effect (to solve SwiftUI’s Bug).

The usage and effect are exactly the same as the official version of SwiftUI animation<V>(_ animation: Animation?, value: V).

If we use the .animation modifier version associated with specific values, does it solve all animation problems?

No, it doesn’t.

In the initial version, SwiftUI only provided one version of the .animation modifier. It creates a transaction when the current view chain changes, regardless of whether the change is caused by a specific associated value.

Later, a version of the modifier with associated values (similar to the custom version above) was provided, which ensures that a transaction is only created when a specific associated value changes. However, if used improperly, problems can still occur.

For example, if we want to create a rectangle that changes color with animation when isActive is true, and scales without animation when scale is true.

Swift
struct ImplicitAnimationBugDemo: View {
    @State private var isActive = false
    @State private var scale = false
    var body: some View {
        VStack {
            Rectangle()
                .fill(isActive ? .red : .blue)
                .frame(width: 200, height: 200)
                .scaleEffect(scale ? 1.5 : 1.0)
                .animation(.smooth, value: isActive)

            Button("Change") {
                isActive.toggle()
                scale.toggle()
            }
        }
    }
}

After executing the above code, we will find that although the .animation only creates a transaction when isActive changes, since isActive and scale both change within the same state change cycle, the scaleEffect will also use that transaction, which does not achieve the desired effect.

https://cdn.fatbobman.com/implicit-animtion-bug-demo1_2023-06-25_18.03.24.2023-06-25_18_04_05.gif

The solution is very simple, just adjust the position of .animation so that the component that needs animation can obtain the correct transaction.

Swift
Rectangle()
    .fill(isActive ? .red : .blue)
    .animation(.smooth, value: isActive) // move animation modifier
    .frame(width: 200, height: 200)
    .scaleEffect(scale ? 1.5 : 1.0

https://cdn.fatbobman.com/implicit-animation-bug-demo2_2023-06-25_18.05.24.2023-06-25_18_06_02.gif

Of course, we can also set different transitions for different animatable components.

Swift
Rectangle()
    .fill(isActive ? .red : .blue)
    .animation(.smooth(duration: 0.2), value: isActive)
    .frame(width: 200, height: 200)
    .scaleEffect(scale ? 1.5 : 1.0)
    .animation(.bouncy(duration: 2), value: scale)

https://cdn.fatbobman.com/implict-animtion-bug-demo3_2023-06-25_18.08.16.2023-06-25_18_09_07.gif

Attention! In SwiftUI, there is a bug where certain animatable components cannot obtain the correct transaction. If we replace scaleEffect with offset, we cannot achieve the same effect as above: different animation components apply different transactions.

Theoretically, when using the following code for translation, there should not be any animation effect.

Swift
struct ImplicitAnimationBugDemo: View {
    @State private var isActive = false
    @State private var scale = false
    var body: some View {
        VStack {
            Rectangle()
                .fill(isActive ? .red : .blue)
                .animation(.smooth, value: isActive)
                .frame(width: 200, height: 200)
                .offset(x: scale ? 200 : 0) // change scaleEffect to offset
                .animation(.none, value: scale)

            Button("Change") {
                isActive.toggle()
                scale.toggle()
            }
        }
    }
}

https://cdn.fatbobman.com/implicit-animtion-bug-demo4_2023-06-25_18.13.37.2023-06-25_18_14_16.gif

How to deal with situations where offset doesn’t cooperate!

Do you still remember the distribution principle of transactions? If we separate the changes of isActive and scale (change them to two separate state adjustments), then different animatable components can obtain the correct transaction.

Swift
Button("Change") {
    isActive.toggle()
      // Adjust one-time state change to two-time state change
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.01){
        scale.toggle()
    }
}

This is because, when the first state changes (isActive), fill gets the transaction created by animation(.smooth, value: isActive). However, when the second state changes, fill has already completed the state change (animation in progress), so it doesn’t need to get the transaction again. Instead, offset gets the transaction generated by animation(.none, value: scale). This allows us to solve the issue where offset cannot correctly obtain the transaction.

New version of Implicit Animation Modifier announced at WWDC 2023.

At WWDC 2023, Apple introduced new versions of animation and transaction for SwiftUI.

Swift
struct ImplicitAnimationNewVersionDemo: View {
    @State private var isActive = false
    @State private var scale = false
    var body: some View {
        VStack {
            Rectangle()
                .animation(.smooth) {
                    $0.foregroundStyle(isActive ? Color.red : Color.blue)
                }
                .frame(width: 200, height: 200)
                .transaction {
                    $0.animation = .none
                } body: {
                    $0.scaleEffect(scale ? 1.5 : 1)
                }

            Button("Change") {
                isActive.toggle()
                scale.toggle()
            }
        }
    }
}

Compared to previous versions, the new version of animation and transaction will only apply newly created transactions within the closure. This ensures that the transaction coming from upstream will continue to be passed along the view chain unchanged, thus ensuring that the developer’s animation intent is correctly passed down.

As of Xcode 15 beta 2, the new version of the modifier is not yet working properly.

What is explicit animation

In a view, a transaction declared with the animation or transaction modifier is called an “implicit animation”. The way of creating a transaction through imperative programming using the global functions withAnimation or withTransaction is called an “explicit animation”.

Compared to “implicit animation”, “explicit animation” has the following differences:

  • Regardless of where the withAnimation function is executed, SwiftUI will start dispatching the transaction created by the “explicit animation” from the root view.
  • When the state changes, SwiftUI automatically distributes the transaction to all affected views.

To create an explicit animation, do the following:

Swift
@main
struct TransactionApp: App {
    var body: some Scene {
        WindowGroup {
            ExplicitAnimationDemo()
                .transactionMonitor("App")
        }
    }
}

struct ExplicitAnimationDemo: View {
    var body: some View {
        VStack {
            Text("Hello World")
                .transactionMonitor("Hello World")
            SubView()
                .transactionMonitor("SubView")
        }
        .transactionMonitor("VStack")
    }
}

struct SubView: View {
    @State private var isActive = false
    var body: some View {
        Rectangle()
            .fill(.cyan)
            .frame(width: 300, height: isActive ? 400 : 200)
            .transactionMonitor("Rectangle")

        Button("Active") {
            withAnimation(.smooth) {
                isActive.toggle()
            }
        }
    }
}

Output:

Bash
App: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
VStack: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
SubView: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
Rectangle: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
Hello World: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
App: nil
VStack: nil
SubView: nil

https://cdn.fatbobman.com/explicit-animtion-demo1_2023-06-25_19.45.37.2023-06-25_19_46_57.gif

Perhaps someone may wonder why almost all view branches are re-assigned transactions, and how SwiftUI decides which view branches to assign “explicit animation” created transactions.

Based on my testing, SwiftUI will assign transactions to all view branches that undergo visual changes during this state change (withAnimation closure triggered).

For example, in the above code, Text("Hello World") will also be assigned a transaction because its position will change after isActive changes. Additionally, all Buttons will be assigned a transaction regardless of whether they change or not (which feels like a bug).

By modifying the code, we can prevent Text("Hello World") from changing its position after isActive changes:

Swift
struct SubView: View {
    @State private var isActive = false
    var body: some View {
        Rectangle()
            .fill(.cyan)
            .frame(width: 300, height: isActive ? 400 : 200)
            .transactionMonitor("Rectangle")
        Spacer() // add Spacer()
        Button("Active") {
            withAnimation(.smooth) {
                isActive.toggle()
            }
        }
    }
}

Output:

Bash
App: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
VStack: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
SubView: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
Rectangle: FluidSpringAnimation(response: 0.5, dampingFraction: 1.0, blendDuration: 0.0)
App: nil
VStack: nil
SubView: nil

https://cdn.fatbobman.com/explicit-animation-demo2_2023-06-25_19.54.17.2023-06-25_19_55_23.gif

By adding a Spacer, we can ensure that the position of Text("Hello world") will not be affected by state changes. This way, SwiftUI will not distribute transactions for Text("Hello World").

Can explicit and implicit animations work together?

Yes.

Developers can change the local transaction by declaring implicit animations on the view branch dispatched by “explicit animations”.

Swift
struct SubView: View {
    @State private var isActive = false
    var body: some View {
        Rectangle()
            .fill(.cyan)
            .frame(width: 300, height: isActive ? 400 : 200)
            .animation(.bouncy, value: isActive) // bouncy will replace smooth

        Button("Active") {
            withAnimation(.smooth) {
                isActive.toggle()
            }
        }
    }
}

Override implicit animations with explicit animations

Compared to “implicit animations”, “explicit animations” require dispatching transactions on more and deeper view branches and hierarchies. Therefore, theoretically, the runtime efficiency of “explicit animations” is lower to achieve the same animation effect.

However, in certain specific situations, using “explicit animations” is more convenient, such as overriding implicit animations with explicit animations.

Do you remember the animation modifier we customized in the previous text? In this implementation, the modifier checks the disablesAnimations property of the upstream transaction. If the property is true, a new transaction is not created.

This custom implementation is based entirely on the implementation logic provided by SwiftUI’s animation modifier.

Swift
struct CoverImplicitAnimationDemo: View {
    @State var isActive = false
    var body: some View {
        VStack {
            Rectangle()
                .fill(isActive ? .red : .blue)
                .frame(width: 300, height: 300)
                .animation(.smooth, value: isActive)

            Button("Cover ImplicitAnimation") {
                var transaction = Transaction(animation: .none)
                transaction.disablesAnimations = true
                withTransaction(transaction) {
                    isActive.toggle()
                }
            }
        }
    }
}

https://cdn.fatbobman.com/explicit-cover-implicit-demo_2023-06-25_20.52.34.2023-06-25_20_53_36.gif

Although we used “implicit animations” to declare transactions for fill, we created and dispatched a transaction with the disablesAnimations property set to true through “explicit animations”. This way, the animation modifier will no longer create new transactions (smooth).

The animation modifier checks the disablesAnimations property, while the transaction modifier requires developers to decide on the logic to use.

Utilize the capabilities of explicit animation for diff and automatic distribution of transactions.

Some of you may wonder why “explicit animations” distribute transactions to all affected views. In fact, this is another advantage of “explicit animations” in some cases.

We can modify the code that combines “explicit animations” with “implicit animations” above to implement pure “implicit animations” (removing withAnimation):

Swift
struct ExplicitAnimationDemo: View {
    var body: some View {
        VStack {
            Text("Hello World")
            SubView()
        }
    }
}

struct SubView: View {
    @State private var isActive = false
    var body: some View {
        Rectangle()
            .fill(.cyan)
            .frame(width: 300, height: isActive ? 400 : 200)
            .animation(.bouncy, value: isActive)

        Button("Active") {
            isActive.toggle()
        }
    }
}

https://cdn.fatbobman.com/implicit-bug-demo5_2023-06-26_07.04.36.2023-06-26_07_05_36.gif

Please note that in the above figure, there is no animation for the displacement of “Hello World”.

This is because in the above code, “implicit animation” is not declared for the VStack outside of the SubView. Therefore, when the size of the Rectangle changes, the VStack adjusts its layout. But because the corresponding transaction is not found, this layout adjustment process is non-animated. This leads to this situation. Using “explicit animation”, SwiftUI will automatically dispatch transactions for VStack.

Of course, if we can adjust the position of the data source, then “implicit animation” can also avoid the above situation.

Swift
struct ExplicitAnimationDemo: View {
    @State private var isActive = false // source of truth
    var body: some View {
        VStack {
            Text("Hello World")
                .transactionMonitor("Hello World")
            SubView(isActive: $isActive)
                .transactionMonitor("SubView")
        }
        .animation(.bouncy, value: isActive) // implicit aniamtion for VStack
    }
}

struct SubView: View {
    @Binding var isActive: Bool
    var body: some View {
        Rectangle()
            .fill(.cyan)
            .frame(width: 300, height: isActive ? 400 : 200)
            .animation(.bouncy, value: isActive)

        Button("Active") {
            isActive.toggle()
        }
    }
}

In this case, “explicit animation” is indeed more convenient than “implicit animation”. However, excessive transaction dispatching may also result in unnecessary animations. By combining “explicit animation” and “implicit animation”, we can more accurately control the animation effects.

Using Explicit Animations to Disable System Component Animations

In iOS 17, SwiftUI will make most system components (such as Sheet, FullScreenCover, NavigationStack, Inspector, etc.) check the disablesAnimations property from the upstream transaction when implementing animations. Finally, developers can use pure SwiftUI to decide whether to use animations during the transition process of these components.

NavigationStack:

Swift
struct NavigationStackDemo: View {
    @State var pathStore = PathStore()
    var body: some View {
        @Bindable var pathStore = pathStore
        NavigationStack(path: $pathStore.path) {
            List {
                Button("Go Link without Animation") {
                    var transaction = Transaction(animation: .none)
                    transaction.disablesAnimations = true
                    withTransaction(transaction) {
                        pathStore.path.append(1)
                    }
                }
                Button("Go Link with Animation") {
                    pathStore.path.append(1)
                }
            }
            .navigationDestination(for: Int.self) {
                ChildView(store: pathStore, n: $0)
            }
        }
    }
}

@Observable
class PathStore {
    var path: [Int] = []
}

struct ChildView: View {
    let store: PathStore
    let n: Int
    @Environment(\.dismiss) var dismiss
    var body: some View {
        List {
            Text("\(n)")
            Button("Dismiss without Animation") {
                var transaction = Transaction(animation: .none)
                transaction.disablesAnimations = true
                withTransaction(transaction) {
                    store.path = []
                }
            }
            Button("Dismiss with Animation") {
                dismiss()
            }
        }
    }
}

https://cdn.fatbobman.com/disable-animation-demo1_2023-06-26_08.55.59.2023-06-26_08_56_52.gif

Sheet:

Swift
struct SheetDemo: View {
    @State private var isActive = false
    var body: some View {
        List {
            Button("Pop Sheet without Animation") {
                var transaction = Transaction(animation: .none)
                transaction.disablesAnimations = true
                withTransaction(transaction) {
                    isActive.toggle()
                }
            }
            Button("Pop Sheet with Animation") {
                isActive.toggle()
            }
        }
        .sheet(isPresented: $isActive) {
            VStack {
                Button("Dismiss without Animation") {
                    var transaction = Transaction(animation: .none)
                    transaction.disablesAnimations = true
                    withTransaction(transaction) {
                        isActive.toggle()
                    }
                }
                Button("Dismiss with Animation") {
                    isActive.toggle()
                }
            }
            .buttonStyle(.borderedProminent)
        }
    }
}

https://cdn.fatbobman.com/disable-aniamtion-demo2_2023-06-26_09.02.07.2023-06-26_09_03_12.gif

How to get Transaction in animatable components

SwiftUI automatically helps animatable components that conform to the Animatable protocol to obtain a transaction and calculate interpolation.

If you wrap UIKit or AppKit components using methods such as UIViewRepresentable, you can obtain the current transaction in the update method. This ensures that the correct context information can be obtained every time the state changes.

Swift
struct MyView:UIViewRepresentable {
    @Binding var isActive:Bool
    func makeUIView(context: Context) -> some UIView {
        return UIView()
    }

    func updateUIView(_ uiView: UIViewType, context: Context) {
        let transaction = context.transaction
        // check animation
        // do something
    }
}

At WWDC 2023, Apple added new methods to Animation that can help developers retrieve values corresponding to specific points in time.

Components that support Transaction or Animation settings

In SwiftUI, some components or types allow developers to set transactions or animations for them, such as Binding, FetchRequest, etc. Developers should choose whether to use their built-in animation settings according to their needs.

For example, for FetchRequest, we can control whether it uses animation effects when data is added or deleted in three ways.

Swift
// Solution 1
@FetchRequest(
    sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
    animation: .none  // animation
)

// Soulution 2
List {
    ForEach(items) { item in
       ....
    }
    .onDelete(perform: deleteItems)
}
.animation(.bouncy, value: items.count) // animation

// Solution 3
withAnimation {
   addNewItem()
}

withAniamtion {
    delItem()
}

By using the latter two solutions, developers will have greater control over animations.

TransactionKey

At WWDC 2023, Apple added TransactionKey to SwiftUI. This allows developers to carry some custom information in transactions.

The way to create TransactionKey is very similar to EnvironmentKey.

Swift
enum TapSource {
    case root
    case welcome
    case other

    var animation: Animation? {
        switch self {
        case .root:
            Animation.smooth(duration: 3)
        case .welcome:
            nil
        case .other:
            Animation.linear
        }
    }
}

struct SourceKey: TransactionKey {
    static var defaultValue: TapSource = .root
}

extension Transaction {
    var source: TapSource {
        get { self[SourceKey.self] }
        set { self[SourceKey.self] = newValue }
    }
}

Usage:

Swift
@Observable
class Store {
    var isActive = false
}

struct KeyDemo: View {
    @State private var store = Store()
    var body: some View {
        VStack {
            Rectangle()
                .fill(store.isActive ? .orange : .cyan)
                .frame(width: 300, height: 300)
                .transaction {
                    $0.animation = $0[SourceKey.self].animation
                }

            RootView(store: store)
            WelcomeView(store: store)
        }
    }
}

struct RootView: View {
    let store: Store
    var body: some View {
        Button("From Root") {
            withTransaction(\.source, .root) {
                store.isActive.toggle()
            }
        }
    }
}

struct WelcomeView: View {
    let store: Store
    var body: some View {
        Button("From Welcome") {
            withTransaction(\.source, .welcome) {
                store.isActive.toggle()
            }
        }
    }
}

https://cdn.fatbobman.com/transactionKey-demo_2023-06-25_21.07.29.2023-06-25_21_08_28.gif

Please read the article ”A Deep Dive Into Observation: A New Way to Boost SwiftUI Performance” to learn about the specific usage of @Observable.

Some Suggestions for Achieving Precise Animation

  • Declare “implicit animation” near the animatable component that needs animation.
  • Use the new “implicit animation” declaration method whenever possible.
  • When the same effect can be achieved, prioritize using “implicit animation”.
  • When using “explicit animation”, avoid animation anomalies in partial views by declaring “implicit animation” locally.
  • Provide richer context information through TransactionKey when necessary.
  • Avoid modifying too many properties in a single state change as much as possible.
  • When animation anomalies occur, first identify the transaction obtained during the state change.
  • Have a clear understanding of animatable components. In addition to supporting animation modifiers, layout containers also do.
  • When wrapping UIKit or AppKit controls, add logic to check the current transaction.
  • In iOS 17, more navigation components have supported using “explicit animation” to shield animation transitions.

Conclusion

This article focuses on the creation and dispatch mechanism of transactions, without much discussion on other attributes within transactions. Regardless of how much information SwiftUI adds to transactions in the future, as long as we understand their principles, we can achieve efficient and precise animations. When unexpected animation behavior occurs, developers also know how to fix it.

Weekly Swift & SwiftUI insights, delivered every Monday night. Join developers worldwide.
Easy unsubscribe, zero spam guaranteed