出现时禁用 SwiftUI 帧动画
Disable SwiftUI frame animation on appear
目标
假设我有一个 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)
✅ 问题解决了。不过感觉很“丑”,好像开销很大
问题
是否有任何其他(更多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)
}
}
}
目标
假设我有一个 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)
✅ 问题解决了。不过感觉很“丑”,好像开销很大
问题
是否有任何其他(更多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)
}
}
}