出现时禁用 SwiftUI 帧动画

Disable SwiftUI frame animation on appear

目标

假设我有一个 ListLazyVGrid,显示嵌套在 ScrollView 中的多个项目。我使用 ForEach 视图来生成单个项目视图:

ForEach(items) { item in
    ItemView(item)
}

items 数组可能是视图本身的 @State 属性 或视图模型上的 @Published 属性 符合 @ObservableObject(在这个例子中我将使用第一个)。

现在,当我通过插入或删除元素来更改 items 数组时,我希望更改以特定的方式进行动画处理,因此我添加了 transitionanimation修饰符如下:

ScrollView {
    LazyVGrid(columns: 2) {
        ForEach(items) { item in
            ItemView(item)
                .transition(.scale)
        }
    }
}
.animation(.default, value: items)

效果很好。

问题

唯一的问题是这段代码还会导致整个 ScrollView 在视图首次出现时从零缩放到其完整大小。 (这是有道理的,因为在从商店中获取项目之前项目数组最初是空的,所以数组确实发生了变化。)

解决方案尝试

为了解决这个问题,我显然需要使动画依赖于 属性,在视图出现和加载项目数组之前不会改变.因此,我创建了这样一个 属性 作为普通布尔值,并在 items 数组更改时切换它,但仅在调用 didAppear 之后:

@State var changedState: Bool = false
@State var didAppear: Bool = false

@State var items: [Item] = [] {
    didSet {
        if didAppear {
            changedState.toggle()
        }
    }
}

然后我把动画修改器的value改成这个新的属性:

.animation(.default, value: changedState)

✅ 问题解决了。不过感觉很“丑”,好像开销很大

问题

是否有任何其他(更多elegant/concise)方法来禁用初始缩放动画?


‍ 编辑:最小代码示例

struct ContentView: View {
    
    @State var items: [Int] = []
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: [GridItem(), GridItem()]) {
                    ForEach(items, id: \.self) { item in
                        Rectangle()
                            .frame(height: 50)
                            .foregroundColor(.red)
                            .transition(.scale)
                    }
                }
            }
            .animation(.default, value: items)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        let newItem = items.last.map { [=14=] + 1 } ?? 0
                        items.append(newItem)
                    } label: {
                        Text("Add Item")
                    }
                }
            }
        }
        .onAppear {
            items = [Int](0...10)
        }
    }
}

这是初始动画的样子:

您的 didSet 不会按您期望的方式工作,这就是我们 .onChange() 的原因,但正如您所怀疑的那样,确实有更简单的方法。您只想将项目附加到列表(显示在屏幕上)的动画。最简单的方法是添加一个 @State 布尔值,并将其用于 .animation() 值。然后,当您像这样添加到数组时,只需在按钮中切换它:

struct ContentView: View {
    @State var items: [Int] = []
    @State var animate = false // Variable for animation
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: [GridItem(), GridItem()]) {
                ForEach(items, id: \.self) { item in
                    Rectangle()
                        .frame(height: 50)
                        .foregroundColor(.red)
                        .transition(.scale)
                }
            }
        }
        // Use animate as a flag to allow items to be the value
        // for .animation
        .animation(.default, value: (animate ? items : []))
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button {
                    let newItem = items.last.map { [=10=] + 1 } ?? 0
                    items.append(newItem)
                    animate.toggle() // <- Switch it here
                } label: {
                    Text("Add Item")
                }
            }
        }
        .onAppear {
            items = [Int](0...10)
            // The DispatchQueue is necessary to delay changing
            // the flag until the initial view is loaded.
            DispatchQueue.main.asyncAfter(deadline: .now()) {
                animate = true
            }
        }
    }
}

编辑:

上面的代码已更改以反映评论。这应该符合您的需要。

我发现将 .animation 修饰符应用到 LazyVGrid 而不是 ScrollView 可以像您预期的那样工作。

struct ContentView: View {
    
    @State var items: [Int] = []
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: [GridItem(), GridItem()]) {
                    ForEach(items, id: \.self) { item in
                        Rectangle()
                            .frame(height: 50)
                            .foregroundColor(.red)
                            .transition(.scale)
                    }
                }
                .animation(.default, value: items) // <- New Place
            }
            // <- Old Place
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        let newItem = items.last.map { [=10=] + 1 } ?? 0
                        items.append(newItem)
                    } label: {
                        Text("Add Item")
                    }
                }
            }
        }
        .onAppear {
            items = [Int](0...10)
        }
    }
}