I'm trying to make a list of Core Data items in SwiftUI where adding new item will also trigger scroll to last item.
Here is a code I have. It's based on sample Core Data app in Xcode. One Entity: Item with one attribute: timestamp.
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)
}
}
It works, but I'm getting errors like this in Xcode:
2022-02-06 16:28:47.280984+0700 scroll[901:2894134] [error] precondition failure: invalid graph update (access from multiple threads?)
How to fix this? My guess is that it's something to do with concurrency. When new item is created it gets new id. Number of item changes. Core Data saves data. UI scrolls with animation. But maybe when scroll is triggered, swift is still not sure which item is last on the list?
So, things I've tried:
If you uncomment
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
And closing bracket as well - I am not getting an error when I'm adding one item at a time. But if I tap + few times, I'm getting errors again.
Another surprising thing is that if I change ScrollView to List, I am not getting errors at all. Even without DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {