Переход между состояниями в SwiftUI при прокрутке onAppear может быть потерян

Если вы используете метод onAppear для прокрутки к представлению SwiftUI с помощью ScrollViewProxy, это может привести к потере перехода между состояниями в представлении при использовании опубликованных значений ObservableObject.

Проблема возникает во всех текущих iOS SDK (17.5/18.0), на симуляторах iOS и на реальных устройствах.

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

Вид SwiftUI:

struct ContentView: View {
    @StateObject private var viewModel = ContentViewModel()
    @Namespace private var state2ID

    var body: some View {
        ScrollViewReader { scrollProxy in
            ScrollView(.vertical) {
                VStack(spacing: 15) {
                    if viewModel.state2 {
                        VStack {
                            Text("State2 is set")
                        }
                        .id(state2ID)
                        .onAppear {
                            withAnimation {
                                scrollProxy.scrollTo(state2ID)
                            }
                        }
                    }

                    VStack(spacing: 0) {
                        Text("State1: \(viewModel.state1)")
                        Text("State1 changes from 'false -> true -> false' when the button is pressed.")
                            .font(.footnote)
                    }

                    Button("Toggle States") {
                        viewModel.toggleStates()
                    }
                    .buttonStyle(.bordered)

                    Color.teal
                        .frame(height: 900)
                }
                .padding()
            }
        }
    }
}

Посмотреть модель:

@MainActor
final class ContentViewModel: ObservableObject {
    @Published private(set) var state1 = false
    @Published private(set) var state2 = false

    private var stateToggle = false

    func toggleStates() {
        Task { @MainActor in
            state1 = true
            defer {
                // This change never becomes visible in the view!
                // state1 will be improperly shown as 'true' when this method returns while it actually is 'false'.
                print("Resetting state1")
                state1 = false
            }

            stateToggle.toggle()

            if stateToggle {
                withAnimation {
                    state2 = true
                }
            } else {
                state2 = false
            }
        }
    }
}

Когда вы нажимаете кнопку «Переключить состояния», в тексте state1 после завершения задачи отображается «Состояние1: true», но должно отображаться «Состояние1: false», поскольку в задаче state1 для false установлено значение toggleStates. На этом этапе фактические значения модели представления и значения, отображаемые в представлении, больше не синхронизируются.

Если вы удалите вызов scrollProxy.scrollTo(state2ID) в методе onAppear, проблема не возникнет. Таким образом, похоже, что это вызвано процессом прокрутки.

Это кажется очень неожиданным и ошибкой, или я упускаю что-то фундаментальное? Есть идеи, почему это так и как использовать метод scrollTo объекта ScrollViewProxy, чтобы эта ошибка не возникала?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
0
51
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Эта проблема может быть проблемой времени, связанной с выполнением изменений модели представления withAnimation.

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

В любом случае вы можете заставить его работать, убрав элементы управления анимацией из модели представления и вместо этого поместив их в представление:

1. В ContentViewModel закомментируйте withAnimation

// ContentViewModel

if stateToggle {
    // withAnimation { // 👈 HERE
        state2 = true
    // }
} else {
    state2 = false
}

Конечно, вы можете упростить код:

stateToggle.toggle()
state2 = stateToggle

...или вы можете отказаться от stateToggle и вместо этого просто переключить state2. Это может помочь сделать модель представления менее запутанной.

2. В ContentView добавьте модификатор .animation к верхнему уровню VStack внутри ScrollView.

// ContentView

ScrollView(.vertical) {
    VStack(spacing: 15) {
        // ...
    }
    .padding()
    .animation(.default, value: viewModel.state2) // 👈 HERE
}

Это дает анимацию слайда как при раскрытии, так и при скрытии. Если вы хотите подавить анимацию скрытия (как это было раньше), вы можете использовать тернарный оператор в модификаторе .animation:

.animation(viewModel.state2 ? .default : nil, value: viewModel.state2)

Несколько очень хороших предложений! Ваше добавленное изменение к вашему ответу, чтобы не использовать анимацию при сокрытии представления, также было именно тем, чего не хватало в вашем ответе и о чем я хотел спросить. stateToggle использовался только в демонстрационных целях, в реальном коде этот механизм не используется. То же самое относится и к state2, которое на самом деле не является значением Bool.

ITGuy 05.09.2024 12:58

Может ли использование модификатора animation иметь побочные эффекты, связанные с анимацией, на других подпредставлениях в VStack?

ITGuy 05.09.2024 13:09

@ITGuy Это повлияет только на представления, внешний вид которых зависит от флага state2. По сравнению с использованием withAnimation, как вы делали раньше, я ожидаю, что он будет вести себя так же.

Benzy Neez 05.09.2024 13:11

Как и вы, я также подозреваю, что к первоначально описанному поведению приводит проблема с синхронизацией. Я думаю, мы согласны с тем, что исходный код не должен приводить к потере изменений состояния в представлении, даже если вы используете withAnimation в модели представления. Я не видел нигде упоминания Apple о том, что не следует этого делать по описанным причинам. В конечном итоге ваше решение помогает избежать проблемы, даже если оно не полностью объясняет проблему, поэтому я отмечу ваш ответ как решение.

ITGuy 05.09.2024 13:29

@ITGuy Спасибо, что приняли ответ! Я также не видел документов, в которых говорилось бы, что withAnimation не следует использовать вне представления. Но рассмотрим случай, когда вы хотите изменить стиль анимации, возможно, на .spring со всевозможными параметрами. Имхо, такие детали представления определенно не относятся к модели представления.

Benzy Neez 05.09.2024 13:34

С точки зрения архитектуры программного обеспечения я с вами абсолютно согласен. Логика анимации на самом деле не принадлежит модели представления.

ITGuy 05.09.2024 13:57

Другие вопросы по теме