Tenho uma animação bem simples que estou tentando fazer, onde o item selecionado em uma lista recebe um plano de fundo aplicado. O objetivo é que a caixa cinza atrás do item selecionado se transforme da posição anterior para a próxima suavemente.
Eu consegui isso usando matchedGeometryEffect
para que o SwiftUI pudesse combinar as visualizações de fundo entre os dois estados, embora tecnicamente eles tenham um caminho de ID diferente. No entanto, esse esforço é frustrado porList
Aqui está o projeto de exemplo. Tudo o que você precisa fazer para quebrar a animação é cercar os botões em uma List.
Por que List quebra essa animação? Existe alguma maneira de contornar isso?
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()
}
}
Quando extraí tudo
VStack
em umView
, a animação simplesmente funciona. Não consigo explicar exatamente por que isso funciona, mas parece que tem algo a ver com como o SwiftUI atualiza as visualizações em umList
.Em qualquer caso, é quase sempre bom dividir suas opiniões em partes menores.
Aqui eu extraí um
ButtonsView
, e usei isso na lista. Eu movi 2 dos 3 estados dentro deButtonsView
, assim como o@Namespace
, mas você pode alterá-los totalmente para@Binding
s e fazer com que a visualização pai os passe.Eu sugeriria remover todo o código extra para animar o caso selecionado, porque é desnecessário. Especificamente:
animatedCurrentCase
eisAnimating
.Task
..disabled
modificador..animation
modificador.Se você então
withAnimation
executar a atualização dentro do fechamento do botão, funcionará.Outras sugestões:
Use
let
onde puder, em vez devar
. Em particular, as propriedades emAnimationButtonStyle
can uselet
..cornerRadius
está obsoleto, use.clipShape
com umRoundedRectangle
em vez disso, ou apenas coloque umRoundedRectangle
em segundo plano.O
.background
modificador que você estava usando também está obsoleto. Sugira uma refatoração usando um trailing closure e if-statement.Aqui está o exemplo atualizado: