我试图实现一个相当简单的动画,其中列表中的选定项会应用背景。目标是选定项后面的灰色框能够平滑地从上一个位置变形到下一个位置。
我使用 来实现这一点,matchedGeometryEffect
以便 SwiftUI 可以匹配两个状态之间的背景视图,即使从技术上讲它们具有不同的 ID 路径。然而,这一努力被以下因素所阻碍:List
这是示例项目。要中断动画,您只需将按钮放在列表中即可。
为什么 List 会破坏此动画?有什么方法可以解决这个问题吗?
struct AnimationButtonStyle: ButtonStyle {
var isCurrent: Bool
var animationNamespace: Namespace.ID
var backgroundView: some View {
Color.gray
.cornerRadius(8)
.matchedGeometryEffect(id: "Shape", in: animationNamespace)
}
func makeBody(configuration: Configuration) -> some View {
configuration.label
.background(
isCurrent ? backgroundView : nil
)
.opacity(configuration.isPressed ? 0.5 : 1.0)
}
}
struct ContentView: View {
enum cases: String, CaseIterable {
case foo = "Foo"
case bar = "Barrrrrr"
case bat = "Battttttttttttttt"
}
@Namespace private var animationNamespace
@State var animatedCurrentCase: cases = .foo
@State var currentCase: cases = .foo
@State var isAnimating: Bool = false
var body: some View {
VStack {
// Without the list this animation will work
List {
Section {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(cases.allCases.enumerated()), id: \.offset) { index, theCase in
var isCurrent: Bool { theCase == animatedCurrentCase }
Button {
isAnimating = true
Task { @MainActor in
animatedCurrentCase = theCase
try? await Task.sleep(nanoseconds: 200_000_000)
currentCase = theCase
isAnimating = false
}
} label: {
Label {
Text(theCase.rawValue)
} icon: {
VStack(alignment: .leading) {
Text("\(index)")
}
.frame(width: isCurrent ? 16 + 4 : 16)
}
}
.disabled(isAnimating)
.buttonStyle(AnimationButtonStyle(isCurrent: isCurrent, animationNamespace: animationNamespace))
.animation(.smooth, value: animatedCurrentCase)
}
}
}
}
Spacer().frame(height: 10)
Text("Selected: \(currentCase.rawValue)")
.font(.title)
}
.padding()
}
}
当我将整个动画提取
VStack
到单独的 中时View
,动画就可以正常工作了。我无法准确解释为什么会这样,但这似乎与 SwiftUI 如何更新 中的视图有关List
。无论如何,将您的观点分成更小的部分几乎总是一件好事。
这里我提取了一个
ButtonsView
,并将其用于列表中。我已将 3 个状态中的 2 个ButtonsView
以及移入@Namespace
,但您完全可以将它们更改为@Binding
并让父视图将它们传递进去。我建议删除所有用于为选定案例设置动画的额外代码,因为这些代码是不必要的。具体来说:
animatedCurrentCase
和isAnimating
。Task
。.disabled
修饰符。.animation
修饰符。如果您随后使用它
withAnimation
在按钮闭包内执行更新,它就会起作用。其他建议:
let
尽可能使用,而不是var
。特别是 中的属性AnimationButtonStyle
可以使用let
。.cornerRadius
已被弃用,请.clipShape
改用RoundedRectangle
,或将放在RoundedRectangle
后台。您使用的修饰符
.background
也已弃用。建议使用尾随闭包和 if 语句进行重构。以下是更新后的示例: