Если вы используете метод 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
, чтобы эта ошибка не возникала?
Эта проблема может быть проблемой времени, связанной с выполнением изменений модели представления 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)
Может ли использование модификатора animation
иметь побочные эффекты, связанные с анимацией, на других подпредставлениях в VStack
?
@ITGuy Это повлияет только на представления, внешний вид которых зависит от флага state2
. По сравнению с использованием withAnimation
, как вы делали раньше, я ожидаю, что он будет вести себя так же.
Как и вы, я также подозреваю, что к первоначально описанному поведению приводит проблема с синхронизацией. Я думаю, мы согласны с тем, что исходный код не должен приводить к потере изменений состояния в представлении, даже если вы используете withAnimation
в модели представления. Я не видел нигде упоминания Apple о том, что не следует этого делать по описанным причинам. В конечном итоге ваше решение помогает избежать проблемы, даже если оно не полностью объясняет проблему, поэтому я отмечу ваш ответ как решение.
@ITGuy Спасибо, что приняли ответ! Я также не видел документов, в которых говорилось бы, что withAnimation
не следует использовать вне представления. Но рассмотрим случай, когда вы хотите изменить стиль анимации, возможно, на .spring
со всевозможными параметрами. Имхо, такие детали представления определенно не относятся к модели представления.
С точки зрения архитектуры программного обеспечения я с вами абсолютно согласен. Логика анимации на самом деле не принадлежит модели представления.
Несколько очень хороших предложений! Ваше добавленное изменение к вашему ответу, чтобы не использовать анимацию при сокрытии представления, также было именно тем, чего не хватало в вашем ответе и о чем я хотел спросить.
stateToggle
использовался только в демонстрационных целях, в реальном коде этот механизм не используется. То же самое относится и кstate2
, которое на самом деле не является значениемBool
.