SwiftUI 因其简洁的动画 API 与极低的动画设计门槛而广受欢迎。可是,跟着应用程序复杂性的增加,开发者逐步发现,尽管动画设计十分简单,但要完成准确细致的动画操控并非易事。一起,在 SwiftUI 的动画体系中,有关 Transaction 的解说很少,无论是官方材料仍是第三方文章,都没有对其运作机制进行体系的论述。

本文将经过探讨 Transaction 的原理、作用、创立和分发逻辑等内容,告诉读者怎么在 SwiftUI 中完成更加精准的动画操控,以及需求留意的其他问题。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的公众号:【肘子的Swift记事本】

Transaction 是什么

  • transaction 是一个值,包含了 SwiftUI 在处理当时状况改动时需求了解的上下文,其中最重要的是用于计算插值的动画函数。
  • 与环境值有些相似,SwiftUI 会在视图层次结构中隐式向下传播 transaction。
  • 所谓的“显式动画”和“隐式动画”的中心区别在于生成 transaction 和派发 transaction 的方位和逻辑不同。
  • transaction 只与当时的状况改动有关。每当状况发生改动时,SwiftUI 会依据是否由“显式动画”主张或是否有声明”隐式动画”等状况按需生成新的 transaction,并在需求的视图层次中进行传递。
  • 下游的 transaction 生成者(.animation.transaction)将依据设置挑选是否选用上游分发的 transaction 或生成新的 transaction。
  • 在状况改动时,与当时改动状况有相关的可动画组件(一般恪守 Animatable 协议)将获取本次状况改动的上下文(transaction),得到动画曲线函数,并运用它来计算插值。
  • transaction 并不能独自生成或派发,它是状况改动的附带信息。

我信任,很多读者在看完上述对 transaction 的描绘后仍然会感到困惑。因而,在接下来的内容中,咱们将更详细地介绍和论述 transaction 的细节和完成,协助你更好地了解。

怎么调查 Transaction 的改动

经过 .transaction 视图润饰器,咱们能够创立一个工具,以协助咱们更好地研讨和了解 transaction。

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

什么是隐式动画

隐式动画是经过 .animation.transaction(一般运用 .animation)润饰器,在视图分支上声明在状况改动时应该创立的 transaction。

SwiftUI 会在以下状况下调用隐式动画创立 transaction:

  • 当时视图分支在状况改动时会发生改动
  • 当时视图分支上声明晰隐式动画

下面的代码将展现隐式动画是怎么创立 transaction 并向下传递的:

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

输出为:

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
  • 经过 Toggle 调整 isActive,应用的状况发生了改动。
  • SwiftUI 发现 Text("Hello") 和包裹它的 VStack 两个视图链会在状况改动时发生改动。
  • VStack 经过 .animation 声明晰在 isActive 改动时应创立的 transaction(动画函数为 linear)。
  • Text("Hello") 经过 .animation 声明晰在 isActive 改动时应创立的 transaction(动画函数为 smooth)。
  • SwiftUI 调用 VStack.animation 创立了新的 transaction,并向下传递。经过 VStack 和 outer 的输出信息能够看到获得了对应的值。
  • SwiftUI 调用 Text("Hello").animation 创立了新的 transaction,并向下传递,该 transaction 替换了 VStack 向下传递的 transaction( 查看 inner 的输出信息 )。
  • 在状况改动结束后,SwiftUI 重置了 VStackText("Hello") 外侧的 transaction(nil)。

几点提示:

  • SwiftUI 或许会在应用初始阶段为部分视图设置 transaction( 值为 nil ),即便没有设置,也不影响视图在状况改动时获取正确的 transaction。
  • SwiftUI 或许会在状况改动后为部分视图重置 transaction( 值为 nil ),即便没有重置,也不影响下次的动画( 下次状况改动时,会生成新的 transaction )。
  • 当传递进来的 transaction 为 nil 时,SwiftUI 会优化调用 .transaction 润饰器闭包的机遇。假如没有在闭包中修正 transaction,或许会疏忽该闭包( 不调用 )。

.animation.transaction 有什么不同

.animation 润饰器是 .transaction 润饰器的快捷方法。同样,用于“显式动画”的 withAnimation 则是 withTransaction 的快捷方法。

例如,咱们能够经过下面的代码为 iOS 13 创立与特定值相关的 .animation 润饰器版别。

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

代码提示:

  • 保存要比较的值。
  • 当相关的值发生改动时,更新保存的值。
  • 查看上游的 transaction 的 disablesAnimations 特点,以判断是否用新的 transaction 替代上游的 transaction(有关 disablesAnimations,下文有更多阐明)。
  • onAppear 是用来保证第一次设置便起作用(处理 SwiftUI 的 Bug)。

运用方法和作用与 SwiftUI 官方版别 animation<V>(_ animation: Animation?, value: V) 彻底一致。

运用与特定值相关的 .animation 润饰器版别,就能够防止动画的反常问题了吗?

并不是。

在最初的版别中,SwiftUI 只供给了一个版别的 .animation。它会在当时视图链发生改动时创立 transaction,而不关心该改动是否由特定的相关值所导致。

后来供给的具备相关值版别的润饰器(相似于上面的自定义版别),将保证只在特定相关值发生改动时才创立 transaction,但假如运用不当,仍会呈现问题。

例如,咱们想要创立一个矩形。当 isActive 为 true 时,经过动画更改颜色;当 scale 为 true 时,不运用动画进行缩放。

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

执行上面的代码后,咱们会发现,尽管 .animation 仅在 isActive 发生改动时才创立 transaction,但由于 isActivescale 在同一个状况改动周期内都发生了改动,因而 scaleEffect 同样会运用该 transaction,并没有达到咱们想要的作用。

掌握 Transaction,实现 SwiftUI 动画的精准控制

处理方法很简单,只需调整.animation的方位,使需求动画的组件获取到正确的 transaction 即可。

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

当然,咱们也能够为不同的可动画组件设置不同的 transition。

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)

掌握 Transaction,实现 SwiftUI 动画的精准控制

需求留意!在 SwiftUI 中,某些可动画组件存在获取 transaction 的 Bug。假如咱们将 scaleEffect 替换为 offset,就无法完成与上面相同的作用:不同的动画组件应用不同的 transaction。

理论上,运用以下代码进行平移操作时不应该带有动画作用。

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

面临 offset 不听话这样的状况该怎么办!

还记得 transaction 的派发原理吗?假如咱们将 isActive 和 scale 的改动分开(改为两次状况调整),那么不同的可动画组件就能够获取到正确的 transaction 了。

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

这是由于,在第一次状况改动时(isActive),fill 将获得由 animation(.smooth, value: isActive) 创立的 transaction。而在第二次状况改动时,fill 已经完成了状况改动(动画进行中),它不需求再次获取 transaction。offset 则获取了由 animation(.none, value: scale) 生成的 transaction。咱们经过这种方法处理了 offset 无法正确获取 transaction 的 Bug。

新的隐式动画声明方法在 WWDC 2023 中被宣布

在 WWDC 2023 中,苹果为 SwiftUI 增加了新的 animationtransaction 版别。

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

与之前的版别比较,新版的 animationtransaction 会将新创立的 transaction 仅应用于闭包内部。这样一来,上游传来的 transaction 将依照原样沿视图链持续传递,然后保证开发者的动画意图被正确地传递下去。

截止到 Xcode 15 beta 2,新版别的润饰符还无法正常工作。

什么是显式动画

在视图中,经过 animationtransaction 润饰器声明的 transaction 被称为“隐式动画”。运用指令式编程的手段,经过大局函数 withAnimationwithTransaction 创立 transaction 的方法则被称为“显式动画”。

相较于“隐式动画”,“显式动画”有以下不同之处:

  • 无论在何处执行 withAnimation 函数,SwiftUI 都将从根视图开端派发“显式动画”创立的 transaction
  • 当状况发生改动时,SwiftUI 会主动为一切受影响的视图分发 transaction

要创立显式动画,请依照以下方法进行:

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

输出为:

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

也许有人会想知道,为什么几乎一切的视图分支都被重新派发了 transaction,SwiftUI 是依据什么来决议哪些视图分支要派发“显式动画”创立的 transaction。

依据我的测试,SwiftUI 将为一切在本次状况改动时( withAnimation 闭包引发)发生视觉改动的视图分支派发 transaction。

例如,上面代码中的 Text("Hello World"),由于在 isActive 发生改动后,它的方位也将改动,因而,该分支也将被派发 transaction。别的,一切的 Button,无论是否发生改动,都将被派发 transaction( 感觉上像 Bug )。

经过修正代码,咱们能够让 Text("Hello World")isActive 改动后,不改动其方位:

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

输出:

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

经过增加 Spacer,咱们能够确保 Text("Hello world") 的方位不会遭到状况改动的影响。这样,SwiftUI 就不会为 Text("Hello World") 分发 transaction 了。

显式动画能够和隐式动画协作吗

能够。

开发者能够经过在“显式动画”派发的视图分支上声明“隐式动画”的方法,来改动部分的 transaction。

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

用显式动画掩盖隐式动画

相较于“隐式动画”,“显式动画”需求在更多、更深的视图分支和层级上派发 transaction。因而,理论上来说,为了达到相同的动画作用,“显式动画”的运转功率要低一点。

然而,在某些特定状况下,运用“显式动画”会更便利,例如:经过显式动画来掩盖隐式动画。

还记得上文中咱们自定义的 animation 润饰器完成吗?在这个完成中,润饰器会判断上游 transaction 的 disablesAnimations 特点。假如该特点为 true,则不创立新的 transaction。

这个自定义完成彻底仿照了 SwiftUI 供给的 animation 润饰器的完成逻辑。

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

尽管咱们运用了“隐式动画”来为 fill 声明晰 transaction,可是经过“显式动画”,咱们创立并派发了一个 disablesAnimations 特点为 true 的 transaction。这样,animation 润饰符将不再创立新的 transaction(smooth)。

animation 润饰符会判断 disablesAnimations 特点,而 transaction 润饰符需求开发者自行决议选用何种逻辑。

利用显式动画的 diff 和主动分发 Transaction 的才干

大家是否会有些古怪,为什么“显式动画”要对一切受影响的视图分发 transaction 呢?事实上,这也是在某些状况下,“显式动画”的另一个优势。

咱们将上面“显式动画”与“隐式动画”协作的代码,改成纯“隐式动画”(去掉 withAnimation)的完成:

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

请留意,上图中,“Hello World” 的位移没有动画。

这是由于在上面的代码中,没有为 SubView 外面的 VStack 声明“隐式动画”。因而,当 Rectangle 的尺度增大时,VStack 会调整布局。但由于没有找到对应的 transaction,此布局调整的进程是非动画的。然后导致了这种状况。运用“显式动画”,SwiftUI 将主动为 VStack 派发 transaction。

当然,假如咱们能够调整数据源的方位,那么 “隐式动画” 同样能够防止上面的状况。

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

在这种状况下,“显式动画”确实比“隐式动画”更便利。可是,过多的 transaction 派发也或许发生不必要的动画。经过将“显式动画”和“隐式动画”结合起来运用,才干更准确地操控动画作用。

运用显式动画屏蔽体系组件动画

在 iOS 17 中,SwiftUI 会让大多数体系组件(如 Sheet、FullScreeCover、NavigationStack、Inspector 等)在完成动画时,首要查看来自上游 transaction 的 disablesAnimations 特点。开发者总算能够用纯 SwiftUI 的方法来决议是否在这些组件的切换进程中运用动画了。

NavigationStack:

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

Sheet:

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

可动画组件怎么获取 Transaction

SwiftUI 会主动协助符合 Animatable 协议的可动画组件获取 transaction,并计算插值。

假如你运用例如 UIViewRepresentable 的方法对 UIKit 或 AppKit 组件进行包装,则能够在 update 方法中获取当时的 transaction。这样就能保证在每次状况发生改动时都能获取正确的上下文信息。

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

在 WWDC 2023 中,苹果为 Animation 增加了新的方法,能够协助开发者获取在特定时刻点对应的值。

支撑设置 Transaction 或 Animation 的组件

在 SwiftUI 中,一些组件或类型答应开发者为其设置 transaction 或 animation,例如:Binding、FetchRequest 等。开发者应依据需求挑选是否选用其内置的动画设置。

例如,对于 FetchRequest,咱们能够经过三种方法来操控其在数据增加或删去时是否选用动画作用。

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

运用后两种方法,开发者将具有更强的动画操控力。

TransactionKey

在 WWDC 2023 上,苹果为 SwiftUI 增加了 TransactionKey。这答应开发者在 transaction 中带着一些自定义信息。

创立 TransactionKey 的方法与 EnvironmentKey 十分相似。

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

运用方法:

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

掌握 Transaction,实现 SwiftUI 动画的精准控制

请阅读 深度解读 Observation —— SwiftUI 功能提高的新途径 一文,了解 @Observable 的详细用法。

完成精准动画的一些主张

  • 在需求运用动画的可动画组件邻近声明“隐式动画”。
  • 或许的状况下,运用新的“隐式动画”声明方法。
  • 在同样的作用下,优先运用“隐式动画”。
  • 在运用“显式动画”时,经过在部分声明“隐式动画”来防止部分视图呈现动画反常。
  • 在需求的状况下,能够经过 TransactionKey 供给更丰厚的上下文信息
  • 尽量不在一次状况改动中修正过多的特点。
  • 呈现动画反常时,应首要明确反常部位在状况改动时所获取到的 transaction。
  • 对可动画部件要有明确的了解,除了支撑动画的润饰器外,布局容器也是。
  • 在包装 UIKit 或 AppKit 控件时,应增加查看当时 transaction 的逻辑。
  • 在 iOS 17 中,更多的导航组件已支撑经过运用“显式动画”来屏蔽动画转场。

最终

本文侧重介绍 transaction 的创立和派发机制,对于 transaction 中的其他特点没有进行更多的讨论。无论 SwiftUI 未来为 transaction 增加多少信息,只需咱们掌握了其原理,就能完成高效精准的动画。在呈现预期之外的动画行为时,开发者也知道该怎么调整。

欢迎你经过 Twitter、 Discord 频道 或博客的留言板与我进行沟通。

订阅下方的 邮件列表,能够及时获得每周最新文章。

原文宣布在我的博客wwww.fatbobman.com

欢迎订阅我的公众号:【肘子的Swift记事本】