Условно показать/скрыть содержимое цикла ForEach с анимацией

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

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

Я создал демонстрационный проект, чтобы показать упрощенную версию проблемы.

struct SectionView: View {
    let title: String
    let data: [String]
    
    @State private var isExpanded: Bool = true
    
    var body: some View {
        VStack {
            HStack {
                Text(title)
                    .font(.title2)
                    .fontWeight(.semibold)
                Text(data.count.description)
                    .font(.body)
                    .fontWeight(.light)
                    .foregroundStyle(.secondary)
                Spacer()
                Button {
                    self.isExpanded.toggle()
                } label: {
                    Label("развернуть", systemImage: "chevron.down")
                        .labelStyle(.iconOnly)
                        .rotationEffect(.degrees(isExpanded ? 0 : -90))
                        .animation(.easeInOut(duration: 0.2), value: isExpanded)
                }
            }
            .padding(.horizontal, 16)
            Divider()
            VStack {
                if isExpanded {
                    ForEach(data, id: \.self) { item in
                        Text(item)
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    let data: [String: [String]] = [
        "Секция A": ["Элемент A.1", "Элемент A.2", "Элемент A.3", "Элемент A.4", "Элемент A.5" ],
        "Секция B": ["Элемент B.1", "Элемент B.2"],
        "Секция C": ["Элемент C.1", "Элемент C.2", "Элемент C.3", "Элемент C.4"]
    ]
    
    var body: some View {
        NavigationStack {
            VStack {
                List {
                    ForEach(Array(data.keys).sorted(by: { $0 < $1 }), id: \.self) { key in
                        SectionView(title: key, data: data[key]!)
                    }
                }
                .padding()
                .listStyle(.inset)
                .navigationTitle("Пример проекта")
            }
        }
    }
}

#Preview {
    ContentView()
}

Когда я нажимаю кнопку переключения, содержимое исчезает мгновенно из-за условной проверки, в которую я обернул ForEach. Есть ли способ анимировать содержимое цикла ForEach, поднимаясь вверх к Divider и исчезая?

Я попробовал следующее внизу VStack, но, похоже, он не осознает, что содержимое исчезает, поэтому не анимируется.

.animation(.easeOut(duration: 0.2), value: isExpanded)
Divider()
VStack {
    if isExpanded {
        ForEach(data, id: \.self) { item in
            Text(item)
        }
    }
}.animation(.easeOut(duration: 0.2), value: isExpanded)

Буду признателен за любой совет по этому вопросу – спасибо!

Ответ или решение

Для реализации анимированного показа и скрытия содержимого ForEach в SwiftUI можно использовать несколько подходов. В данном случае, основная проблема заключается в том, что обычное условное выражение if не поддерживает анимацию при смене состояния isExpanded. Вместо этого можно использовать метод withAnimation и анимацию Opacity для достижения желаемого эффекта.

Шаги по реализации анимации

Вот как можно модифицировать ваш код, чтобы достичь плавного появления и исчезновения содержимого в ForEach:

1. Изменение состояния с анимацией

Вместо того чтобы менять состояние видимости ForEach с помощью if, воспользуемся анимацией для изменения непрозрачности элементов внутри VStack.

2. Код

Ниже приводится обновленный пример вашего SectionView, который включает плавную анимацию скрытия и показа элементов списка:

struct SectionView: View {
    let title: String
    let data: [String]

    @State private var isExpanded: Bool = true

    var body: some View {
        VStack {
            HStack {
                Text(title)
                    .font(.title2)
                    .fontWeight(.semibold)
                Text(data.count.description)
                    .font(.body)
                    .fontWeight(.light)
                    .foregroundStyle(.secondary)
                Spacer()
                Button {
                    withAnimation(.easeInOut(duration: 0.2)) {
                        self.isExpanded.toggle()
                    }
                } label: {
                    Label("expand", systemImage: "chevron.down")
                        .labelStyle(.iconOnly)
                        .rotationEffect(.degrees(isExpanded ? 0 : -90))
                        .animation(.easeInOut, value: isExpanded)
                }
            }
            .padding(.horizontal, 16)
            Divider()
            VStack {
                ForEach(data, id: \.self) { item in
                    Text(item)
                        .transition(.opacity)
                        .animation(.easeOut(duration: 0.2), value: isExpanded)
                        .opacity(isExpanded ? 1 : 0) // Изменение непрозрачности
                }
            }
            .animation(.easeOut(duration: 0.2), value: isExpanded) // Анимация при изменении состояния
        }
    }
}

3. Объяснение изменений:

  • withAnimation: Окружив self.isExpanded.toggle() с withAnimation, вы вводите возможность анимации смены состояния.

  • opacity: Добавление изменения непрозрачности для текстового элемента позволяет управлять его видимостью с плавным переходом. Когда isExpanded становится false, opacity становится 0, и элемент плавно исчезает.

  • transition(.opacity): Использование transition позволяет настраивать анимацию появления и исчезновения. В данном случае мы указываем анимацию с изменением непрозрачности.

Заключение

Эти изменения помогут вам достичь плавной анимации при скрытии и показе элементов в ForEach. Такой подход делает интерфейс более отзывчивым и приятным для пользователей. Не забывайте тестировать свою реализацию на разных устройствах, чтобы убедиться, что анимация выглядит хорошо.

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

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