英文:
Swift UI + Core Data in a background thread?
问题
在我的SwiftUI应用程序中,有几个地方需要从CoreData中获取一组不断增长的信息并在更新UI之前进行处理。
我正在使用MVVM方法来处理这个问题,因此有许多NSFetchedResultsController
实例负责获取和更新结果。
为了减少主线程的阻塞,我尝试将NSFetchedResultsController
切换到在后台线程上执行其工作。
这个方法有效,但只有在禁用-com.apple.CoreData.ConcurrencyDebug 1
参数以确保不违反线程规则时才有效。
我已经确保所有与CoreData的访问都在后台线程上进行,但似乎当SwiftUI视图访问CD对象的属性时,它是在主线程上进行的,因此导致崩溃。
目前我可以想到几种可能的解决方案:
- 确保
NSFetchedResultsController
获取/处理的数据可以在很短的时间内获取,以确保UI不会挂起。 - 创建“DTO”对象,使获取的数据然后插入到后台线程内的另一个类实例中,然后UI使用该对象。
- 这种方法的问题在于,随后对对象的编辑或对更新的反应变得更加复杂,因为您需要手动保持CD和DTO对象的同步。
**编辑:**添加了我正在使用的ViewModel示例
class ContentViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
@Published var items: [Item] = []
private let viewContext = PersistenceController.shared.container.viewContext
private let resultsController: NSFetchedResultsController<Item>!
private let backgroundContext: NSManagedObjectContext!
override init() {
let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
let sort = NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)
fetchRequest.sortDescriptors = [sort]
fetchRequest.propertiesToFetch = ["timestamp"]
backgroundContext = PersistenceController.shared.container.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
resultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: backgroundContext,
sectionNameKeyPath: nil,
cacheName: nil)
super.init()
resultsController.delegate = self
try? resultsController.performFetch()
DispatchQueue.main.async { [self] in
items = resultsController.fetchedObjects ?? []
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
DispatchQueue.main.async { [self] in
withAnimation {
items = (controller.fetchedObjects as? [Item]) ?? []
}
}
}
}
英文:
I have a couple of places in my SwiftUI app where I need to fetch a (growing) set of information from CoreData and process it before updating the UI.
I am using the MVVM approach for this and so have a number of NSFetchedResultsController
instances looking after fetching and updating results.
In order to stop blocking the main thread as much I tried shifting the NSFetchedResultsController
to perform its work on a background thread instead.
This works but only if I disable the -com.apple.CoreData.ConcurrencyDebug 1
argument to ensure I'm not breaching threading rules.
I am already ensuring that all CD access is done on the background thread however it appears that when the SwiftUI view accesses a property from the CD object it is doing so on the main thread and so causing a crash.
For now I can think of a couple of possible solutions:
- Ensure that the data fetched by the
NSFetchedResultsController
can be fetched/processed in a small amount of time to ensure the UI doesn't hang - Make "DTO" objects so that data fetched is then inserted into another class instance inside the background thread and have the UI use that.
- The issue with this approach is then making edits or reacting to updates on the object become a lot more convoluted as you need to manually keep the CD and the DTO objects in-sync.
EDIT: Added example of ViewModel I'm using
class ContentViewModel: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
@Published var items: [Item] = []
private let viewContext = PersistenceController.shared.container.viewContext
private let resultsController: NSFetchedResultsController<Item>!
private let backgroundContext: NSManagedObjectContext!
override init() {
let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
let sort = NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)
fetchRequest.sortDescriptors = [sort]
fetchRequest.propertiesToFetch = ["timestamp"]
backgroundContext = PersistenceController.shared.container.newBackgroundContext()
backgroundContext.automaticallyMergesChangesFromParent = true
resultsController = NSFetchedResultsController(
fetchRequest: fetchRequest,
managedObjectContext: backgroundContext,
sectionNameKeyPath: nil,
cacheName: nil)
super.init()
resultsController.delegate = self
try? resultsController.performFetch()
DispatchQueue.main.async { [self] in
items = resultsController.fetchedObjects ?? []
}
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
DispatchQueue.main.async { [self] in
withAnimation {
items = (controller.fetchedObjects as? [Item]) ?? []
}
}
}
}
答案1
得分: 1
问题的核心是您正在后台队列上获取项目,然后在主队列上使用它们。这在Core Data中是不允许的。如果没有并发调试标志,它可能不会立即崩溃,但它正在设置一个在某个时候可能发生的崩溃--这就是为什么并发调试会抱怨的原因。
要在一个队列上获取数据,然后在另一个队列上使用结果,您可以使用对象ID。它们是线程安全的。您可以这样使用:
@Published var itemIDs: [NSManagedObjectID] = []
然后跟着这样做:
itemIDs = resultsController.fetchedObjects.map { $0.objectID } ?? []
然后在主队列中,通过它们的ID查找对象。您可以对对象数组执行此操作,方法是使用谓词进行获取,谓词类似于 NSPredicate(format: "self in %@", itemIDs)
。这应该很快,因为在这一点上不需要进行任何过滤,而且由于内部的Core Data缓存,它也会很快。
英文:
The core problem is that you're fetching items on a background queue and then using them on the main queue. That's not allowed with Core Data. It might not crash immediately without the concurrency debug flag, but it's setting up a crash that's likely to happen at some point-- which is why concurrency debugging complains.
To fetch on one queue but use the results on the other, you can use the object IDs. They're thread safe. You'd use something like
@Published var itemIDs: [NSManagedObjectID] = []
And follow that with something like
itemIDs = resultsController.fetchedObjects.map { $0.objectID } ?? []
Then over in the main queue, look up objects by their IDs. You can do this for an array of objects by doing a fetch where the predicate is something like NSPredicate(format: "self in %@", itemIDs)
. It should be fast because there's no need do any filtering at that point and because of internal Core Data caching.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论