假设我有一个
List
或 LazyVGrid
,它显示嵌套在 ScrollView
内的多个项目。我使用 ForEach
视图来生成单个项目视图:
ForEach(items) { item in
ItemView(item)
}
items
数组可能是视图本身的 @State
属性,或者是符合 @Published
的视图模型上的 @ObservableObject
属性(在本例中我将使用第一个)。
现在,当我通过插入或删除元素来更改
items
数组时,我希望以特定的方式对更改进行动画处理,因此我添加了 transition
和 animation
修饰符,如下所示:
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)
✅ 这样就解决了问题。不过,感觉很“丑”,而且开销很大。
问题
👩u200d💻编辑:最小代码示例
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 { $0 + 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 { $0 + 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 { $0 + 1 } ?? 0
items.append(newItem)
} label: {
Text("Add Item")
}
}
}
}
.onAppear {
items = [Int](0...10)
}
}
}
您的代码应如下所示:
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
// Make the changes that you don't want animated
}
这应该适用于任何标准动画,包括显示/消除工作表和全屏封面。