Вопрос или проблема
У меня есть довольно простая анимация, которую я пытаюсь реализовать, где выбранному элементу в списке применяется фон. Цель заключается в том, чтобы серый прямоугольник за выбранным элементом плавно переходил из предыдущего положения в следующее.
Я добился этого с помощью matchedGeometryEffect
, чтобы SwiftUI мог сопоставить фоновый вид между двумя состояниями, хотя технически у них разные идентификаторы. Однако это усилие мешает 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 {
// Без списка эта анимация будет работать
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("Выбранный: \(currentCase.rawValue)")
.font(.title)
}
.padding()
}
}
Ответ или решение
Почему использование List
мешает анимации matchedGeometryEffect в SwiftUI?
При разработке анимации в SwiftUI, особенно когда речь идет о визуальном взаимодействии состояний, бывает сложно сохранить плавность анимации, особенно если использовать компоненты как List
. В приведенном вами коде, вы хотите плавно анимировать изменение фона для выбранного элемента списка. Однако, когда вы оборачиваете ваши кнопки в List
, анимация matchedGeometryEffect
перестает работать должным образом. Давайте детально разберем, почему это происходит и как можно обойти эту проблему.
Причины проблемы
-
Специфика
List
в SwiftUI:
List
– это сложный компонент, который использует собственные механизмы управления состоянием и жизненным циклом представлений. Когда вы помещаете ваши представления, в частности кнопки, вList
,List
создает и управляет внутренними представлениями для оптимизации производительности. Однако это может влиять на анимацию, посколькуList
может неверно обрабатывать переходы между состояниями для представлений, обернутых в его структуру. -
Изоляция анимационных эффектов:
ИспользованиеmatchedGeometryEffect
требует, чтобы представления имели общую иерархию и были «узнаны» в одном конкретном контексте анимации. Когда представление помещается внутриList
, SwiftUI может не «узнавать» переходы между различными состояниями и игнорировать соответствие идентификаторов. Это приводит к тому, что анимация просто не запускается, так как SwiftUI не может определить, что оно должно анимироваться через разные состояния.
Возможные пути обхода проблемы
Существует несколько способов, которые позволят обойти проблему с анимацией matchedGeometryEffect
в контексте List
в SwiftUI:
-
Использовать
ForEach
вместоList
:
Один из самых простых способов – заменитьList
наForEach
. Это позволит сохранить анимацию в тех же рамках анимационных эффектов, которые предоставляет SwiftUI, так какForEach
не вводит те ограничения, которые накладываетList
.Пример:
VStack { ForEach(cases.allCases.indices, id: \.self) { index in // Ваш код кнопок здесь... } }
-
Раздвинуть эффект в другой контекст:
Если вам нужно сохранитьList
, попробуйте переместить анимацию иmatchedGeometryEffect
для корневого представления или использоватьZStack
, чтобы более точно контролировать контекст анимации. -
Работа с .id():
Можно применить.id(...)
в представлениях внутриList
для задания уникальных идентификаторов, что может улучшить работу анимации. Однако следует учитывать, что это может вести к ухудшению производительности из-за перерасчетов.
Заключение
Использование List
в SwiftUI может существенно усложнить анимацию из-за особенностей его реализации жизненного цикла представлений и механизма управления состоянием. Тем не менее, есть и альтернативы, которые помогут вам достичь желаемых результатов анимации без потерь в производительности. Используя более простые конструкции, такие как ForEach
, или перемещая контекст анимации, вы сможете восстановить желаемую анимацию и сделать пользовательский интерфейс более привлекательным.