SwiftUI, Core Data, ScrollViewReader — [ошибка] сбой предварительного условия: недопустимое обновление графа (доступ из нескольких потоков?)

Я пытаюсь составить список элементов Core Data в SwiftUI, где добавление нового элемента также вызовет прокрутку до последнего элемента.

Вот код, который у меня есть. Он основан на образце приложения Core Data в Xcode. Одна сущность: элемент с одним атрибутом: метка времени.

      import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
        animation: .default)
    private var items: FetchedResults<Item>
    
    var body: some View {
        NavigationView {
            ScrollViewReader { proxy in
                ScrollView {
                    ForEach(items, id: \.self) { item in
                        Text("Some item")
                            .padding()
                            .id(item.objectID)
                    }
                    .onDelete(perform: deleteItems)
                }
                .onChange(of: items.count) { _ in
//                  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                        withAnimation {
                            proxy.scrollTo(items.last?.objectID)
                        }
//                  }
                }
                .toolbar {
                    ToolbarItem {
                        Button(action: addItem) {
                            Label("Add Item", systemImage: "plus")
                        }
                    }
                }
            }
            Text("Select an item")
        }
    }
    
    private func addItem() {
        withAnimation {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
            
            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
    
    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            offsets.map { items[$0] }.forEach(viewContext.delete)
            
            do {
                try viewContext.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nsError = error as NSError
                fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

Это работает, но я получаю такие ошибки в Xcode:

2022-02-06 16:28:47.280984+0700 scroll[901:2894134] [ошибка] ошибка предварительного условия: недопустимое обновление графа (доступ из нескольких потоков?)

Как это исправить? Я предполагаю, что это как-то связано с параллелизмом. Когда создается новый элемент, он получает новый идентификатор. Количество изменений товара. Core Data сохраняет данные. Прокрутка пользовательского интерфейса с анимацией. Но, может быть, когда срабатывает прокрутка, Swift все еще не уверен, какой элемент является последним в списке?

Итак, что я пробовал:

Если вы раскомментируете

      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {

И закрывающая скобка тоже - я не получаю ошибку, когда добавляю по одному элементу за раз. Но если я нажму + несколько раз, я снова получаю ошибки.

Еще одна удивительная вещь заключается в том, что если я изменяю ScrollView на List , я вообще не получаю ошибок. Даже без DispatchQueue.main.asyncAfter(крайний срок: .now() + 0,5) {

2 ответа

Я думаю, что использование Combine Framework с оператором debounce(for:scheduler:options:) должно решить вашу проблему.

У меня была такая же проблема с моим кодом, и следующее решение отлично работает.

      import SwiftUI
import Combine

struct ChatView: View {
@StateObject var messagesManager: MessagesManager
@State private var cancellables: Set<AnyCancellable> = []

var body: some View {
    VStack {
        VStack {
            TitleRow()
            
            ScrollViewReader { proxy in
                ScrollView {
                VStack {
                    ForEach(messagesManager.messages) { message in
                        MessageBubble(message: message)
                    }
                }
                .padding(.top, 10)
                .background(.white)
                .cornerRadius(30, corners: [.topLeft, .topRight]) // Custom cornerRadius modifier added in Extensions file
                .onChange(of: messagesManager.lastMessageId) { id in
                    // When the lastMessageId changes, scroll to the bottom of the conversation
                    messagesManager.$lastMessageId.debounce(for: .seconds(0.2), scheduler: RunLoop.main)
                        .sink { _ in
                            withAnimation {
                                proxy.scrollTo(id, anchor: .bottom)
                            }
                        }.store(in: &cancellables)
                    
                    }
                }
            }
        }

        .background(Color("blue-curious"))
        
        MessageField()
            .environmentObject(messagesManager)
    }.onTapGesture {
        dissmissKeybord()
    }
}

func dissmissKeybord() {
    UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}

}

Похоже, что ScrollViewхочет вокруг ForEachИ что .scrollTo()не нравится необязательный И вам нужен DispatchQueue.main.asyncAfter(deadline:)с реальной задержкой. Я нашел несколько ссылок на его исправление для VStack, но нет объяснения, почему это работает. Однако я думаю, что это независимые проблемы, которые вызывают одну и ту же ошибку, поэтому все исправления должны были быть сделаны.

      struct ContentView: View {

    // Nothing changed here
    
    var body: some View {
        NavigationView {
            ScrollViewReader { proxy in
                ScrollView {
                    VStack {
                        ForEach(items, id: \.self) { item in
                            Text("Some item")
                                .padding()
                                .id(item.objectID)
                        }
                        .onDelete(perform: deleteItems)
                    }
                }
                .onChange(of: items.count) { _ in
                    if let last = items.last {
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
                            withAnimation {
                                proxy.scrollTo(last.objectID)
                            }
                        }
                    }
                }

    // Nothing changed here
    
}
Другие вопросы по тегам