我有一个简单的视图,可以在需要时为其颜色设置动画:
struct SelfAnimatingView: View {
let name: String
let animate: Bool
var body: some View {
Text(name)
.padding()
.foregroundStyle(.white)
.background(animate ? .red : .blue)
}
}
以及可移动物品的简单列表:
struct ContentView: View {
@State private var removable = false
@State private var names = ["Ted", "Barney", "Lily", "Robin", "Marshal"]
@State private var animatedNames = Set<String>()
private var unanimatedNames: [String] { names.filter({ !animatedNames.contains($0) }) }
var body: some View {
VStack {
Toggle("Removable", isOn: $removable).padding()
ForEach(removable ? unanimatedNames : names, id: \.self) { name in
Button { animatedNames.insert(name) } label: {
SelfAnimatingView(name: name, animate: animatedNames.contains(name))
}
}
Spacer()
}
.animation(.default.delay(1), value: unanimatedNames)
}
}
当
SelfAnimatingView
开关按预期关闭时,Removable
会出现动画:
但是一旦涉及到
delete
逻辑,独立动画就停止工作:
看来 SwiftUI 不会对(逻辑上)已删除的项目应用任何更改。
那么我们如何才能使其应用自包含在被删除之前视觉上发生变化?
如果数组
unanimatedNames
是一个状态变量,每次选择一个项目时都会重新构建,而不是将其作为计算属性,那么您就可以让它工作。
.onChange
处理程序中重建它,该处理程序由 animatedNames
的更改触发。initial
标志在首次显示时构建它,在以前的 iOS 版本中,可以使用 .onAppear
来代替。struct ContentView: View {
@State private var removable = false
@State private var names = ["Ted", "Barney", "Lily", "Robin", "Marshal"]
@State private var animatedNames = Set<String>()
@State private var unanimatedNames = [String]()
var body: some View {
VStack {
// content as before
}
.animation(.default.delay(1), value: unanimatedNames)
.onChange(of: animatedNames, initial: true) {
unanimatedNames = names.filter { !animatedNames.contains($0) }
}
}
}
虽然“SwiftUI 不更新已删除的视图”的问题可以通过编写自己的自定义来解决
Transition
,但颜色变化将在其他视图向上移动以填补其位置的同时发生。
您无法控制
ForEach
向上移动其他按钮的动画,与按下的按钮改变其颜色分开。这必须在两个单独的动画中完成。
您可以创建一个自定义
View
来包裹 Button
。这将首先使用单独的 @State
来动画颜色变化,然后才通过调用 animatedNames.insert(name)
将其自身从视图层次结构中删除。
struct CustomButton: View {
let name: String
@Binding var names: Set<String>
@State private var flag = false
var body: some View {
Button {
// this guard is not needed in this case, because
// calling names.insert(name) twice will have the same effect as
// calling it only once.
// In general, you need this guard to prevent side effects from
guard !flag else { return }
withAnimation {
flag = true
} completion: {
names.insert(name)
}
} label: {
SelfAnimatingView(name: name, animate: flag)
}
}
}
用途:
// also consider wrapping this whole if statement in the custom view
if removable {
CustomButton(name: name, names: $animatedNames)
} else {
Button { animatedNames.insert(name) } label: {
SelfAnimatingView(name: name, animate: animatedNames.contains(name))
}
}