Помещение представлений в список предотвращает анимацию matched geometry между состояниями с помощью matchedGeometryEffect.

Вопрос или проблема

У меня есть довольно простая анимация, которую я пытаюсь реализовать, где выбранному элементу в списке применяется фон. Цель заключается в том, чтобы серый прямоугольник за выбранным элементом плавно переходил из предыдущего положения в следующее.

Я добился этого с помощью 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 перестает работать должным образом. Давайте детально разберем, почему это происходит и как можно обойти эту проблему.

Причины проблемы

  1. Специфика List в SwiftUI:
    List – это сложный компонент, который использует собственные механизмы управления состоянием и жизненным циклом представлений. Когда вы помещаете ваши представления, в частности кнопки, в List, List создает и управляет внутренними представлениями для оптимизации производительности. Однако это может влиять на анимацию, поскольку List может неверно обрабатывать переходы между состояниями для представлений, обернутых в его структуру.

  2. Изоляция анимационных эффектов:
    Использование matchedGeometryEffect требует, чтобы представления имели общую иерархию и были «узнаны» в одном конкретном контексте анимации. Когда представление помещается внутри List, SwiftUI может не «узнавать» переходы между различными состояниями и игнорировать соответствие идентификаторов. Это приводит к тому, что анимация просто не запускается, так как SwiftUI не может определить, что оно должно анимироваться через разные состояния.

Возможные пути обхода проблемы

Существует несколько способов, которые позволят обойти проблему с анимацией matchedGeometryEffect в контексте List в SwiftUI:

  1. Использовать ForEach вместо List:
    Один из самых простых способов – заменить List на ForEach. Это позволит сохранить анимацию в тех же рамках анимационных эффектов, которые предоставляет SwiftUI, так как ForEach не вводит те ограничения, которые накладывает List.

    Пример:

    VStack {
       ForEach(cases.allCases.indices, id: \.self) { index in
           // Ваш код кнопок здесь...
       }
    }
  2. Раздвинуть эффект в другой контекст:
    Если вам нужно сохранить List, попробуйте переместить анимацию и matchedGeometryEffect для корневого представления или использовать ZStack, чтобы более точно контролировать контекст анимации.

  3. Работа с .id():
    Можно применить .id(...) в представлениях внутри List для задания уникальных идентификаторов, что может улучшить работу анимации. Однако следует учитывать, что это может вести к ухудшению производительности из-за перерасчетов.

Заключение

Использование List в SwiftUI может существенно усложнить анимацию из-за особенностей его реализации жизненного цикла представлений и механизма управления состоянием. Тем не менее, есть и альтернативы, которые помогут вам достичь желаемых результатов анимации без потерь в производительности. Используя более простые конструкции, такие как ForEach, или перемещая контекст анимации, вы сможете восстановить желаемую анимацию и сделать пользовательский интерфейс более привлекательным.

Оцените материал
Добавить комментарий

Капча загружается...