Вопрос или проблема
Я работаю над списком SwiftUI с выбором, где ‘выбор’ основан на объекте viewModel
, который является @ObservableObject
. Это работает, но выдает предупреждение в консоли:
Публикация изменений изнутри обновлений представлений не разрешена, это приведет к неопределенному поведению
Это предупреждение появляется только на Mac, при запуске на устройстве (не в предварительном просмотре) и не появляется, когда тот же самый код запускается на iOS.
Вот код для воспроизведения этой проблемы, основанный на шаблоне проекта Core Data:
import SwiftUI
import CoreData
final class SelectionModel: NSObject, ObservableObject {
@Published public var selectedItems = Set<Item.ID>()
}
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
@ObservedObject var viewModel: SelectionModel = SelectionModel()
//@State var selectedItems = Set<Item.ID>() // использование этого работает без предупреждения в консоли
var body: some View {
NavigationStack {
List(selection: $viewModel.selectedItems) {
ForEach(items) { item in
Text(item.timestamp!, formatter: itemFormatter)
}
}
.toolbar {
ToolbarItem {
Button(action: addItem) {
Label("Добавить элемент", systemImage: "plus")
}
}
}
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
let nsError = error as NSError
fatalError("Неразрешенная ошибка \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
#Preview {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
Есть ли поводы для беспокойства по этому поводу и/или есть ли способ избежать этого, продолжая использовать ObservableObject
? Я не могу перейти на @Observable
, потому что мне все еще нужно поддерживать iOS16.
Ответ или решение
При разработке приложений на SwiftUI, особенно при использовании таких компонентов, как List
с функцией выбора элементов на macOS, можно столкнуться с предупреждением в консоли, связанным с выделением:
"Publishing changes from within view updates is not allowed, this will cause undefined behavior".
Это предупреждение указывает на то, что вы пытаетесь изменить состояние @Published
свойства внутри процесса обновления самого представления. Давайте разберемся с тем, в чем проблема, и как ее можно устранить.
Причина предупреждения
Ваша модель SelectionModel
, которая является @ObservableObject
, имеет свойство selectedItems
, помеченное как @Published
. Когда вы используете его в вашем List
с выбором, SwiftUI пытается обновить это свойство во время обработки интерфейса пользователя, что и вызывает предупреждение. Это поведение особенно заметно в macOS, где обработка событий выбора может работать иначе, чем на iOS.
Решение
Чтобы избежать этого предупреждения и сохранить требуемую функциональность, можно рассмотреть следующий подход:
-
Используйте
@State
для хранения состояния выбора: Как и упомянуто в вашем коде, если вы замените@ObservedObject
на@State
, это поможет избежать предупреждения. Это возможно, если вы не нуждаетесь в других местах вашего кодаselectedItems
изSelectionModel
. -
Перенос логики в отдельные функции: Если оставите
viewModel
и@Published
, убедитесь, что любые изменения вselectedItems
происходят вне обновления представления. Вы можете создать отдельные методы вSelectionModel
для управления выбором:final class SelectionModel: NSObject, ObservableObject { @Published public var selectedItems = Set<Item.ID>() func selectItem(_ id: Item.ID) { // Изменение состояния выбора вне обновления представления DispatchQueue.main.async { self.selectedItems.insert(id) } } func deselectItem(_ id: Item.ID) { DispatchQueue.main.async { self.selectedItems.remove(id) } } }
-
Срабатывать на события выбора: Вы можете обработать событие выбора с помощью привязки:
List(selection: Binding( get: { self.viewModel.selectedItems }, set: { newSelection in self.viewModel.selectedItems = newSelection })) { ForEach(items) { item in Text(item.timestamp!, formatter: itemFormatter) .onTapGesture { if self.viewModel.selectedItems.contains(item.id) { self.viewModel.deselectItem(item.id) } else { self.viewModel.selectItem(item.id) } } } }
Подводя итоги
Использование @ObservedObject
с @Published
может быть функциональным, но важно понимать, что любые изменения модели состояния внутри обновления представления могут привести к неопределенному поведению и предупреждениям, как в вашем случае. Приведенные выше подходы помогут решить проблему и избежать предупреждений в консоли, сохраняя при этом функциональность вашего приложения.
Если у вас есть требования к поддержке iOS16, может потребоваться сохранить текущую структуру, но следуя методам, приведенным выше, вы сможете адаптировать код и избежать проблем в будущем.