Выбор списка SwiftUI на Mac выдает предупреждение в консоли о “Недопустимо публиковать изменения изнутри обновлений представлений”.

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

Я работаю над списком 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.

Решение

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

  1. Используйте @State для хранения состояния выбора: Как и упомянуто в вашем коде, если вы замените @ObservedObject на @State, это поможет избежать предупреждения. Это возможно, если вы не нуждаетесь в других местах вашего кода selectedItems из SelectionModel.

  2. Перенос логики в отдельные функции: Если оставите 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)
           }
       }
    }
  3. Срабатывать на события выбора: Вы можете обработать событие выбора с помощью привязки:

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

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

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