Когда мы создаем новый проект в Xcode с установленной опцией Core Data, он генерирует новый проект, определяющий стек Core Data в AppDelegate.swift:
class AppDelegate: UIResponder, UIApplicationDelegate {
// ...
// MARK: - Core Data stack
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "CoreDataTest")
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
})
return container
}()
// MARK: - Core Data Saving support
func saveContext () {
let context = persistentContainer.viewContext
if context.hasChanges {
do {
try context.save()
} catch {
let nserror = error as NSError
fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
}
}
}
}
Чтобы легко получить доступ к persistentContainer
, я также добавил этот фрагмент кода:
static var persistentContainer: NSPersistentContainer {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { fatalError("Could not convert delegate to AppDelegate") }
return appDelegate.persistentContainer
}
Я могу назвать это так:
let container = AppDelegate.persistentContainer
Проблема возникает, когда я пытаюсь получить к нему доступ из фонового потока. Например, у меня есть фрагмент кода, который выполняется в фоновом режиме и извлекает некоторые данные из веб-службы. Когда он получает данные, я сохраняю их, используя:
static func loadData(_ id: String) {
fetchDataOnBackground(id: id) { (error, response) in
if let error = error {
// handle...
return
}
guard let data = response?.data else { return }
let container = AppDelegate.persistentContainer // Here
container.performBackgroundTask({ context in
// save data...
})
}
}
Когда я пытаюсь получить постоянный контейнер, он генерирует на консоли:
Main Thread Checker: UI API called on a background thread: -[UIApplication delegate]
Чтобы этого больше не происходило, я поменял свой persistentContainer
с lazy var
на static
на AppDelegate
:
static var persistentContainer: NSPersistentContainer = {
// same code as before...
}()
И ошибки больше не происходит.
Но мне интересно, может ли это иметь побочные эффекты, о которых я не знаю. Я имею в виду, что у меня в любом случае был бы только один persistentContainer
, потому что есть только один экземпляр AppDelegate
, верно? Значит, я могу изменить его на статический, как это сделал я, и получить к нему доступ с помощью AppDelegate.persistentContainer
в других частях моего приложения без каких-либо проблем?
Или есть другой рекомендуемый шаблон для обработки и использования persistentContainer
?
PersistentContainer
работает с основной очередью. Как следует из названия свойства, этот контекст управляемого объекта предназначен для использования в сочетании с пользовательским интерфейсом приложения. Возможно, вам нужно отправить обратно в основную очередь для взаимодействия с UIApplicationDelegate
и PersistentContainer
, используя:
DispatchQueue.main.async {
// save data …
}
Привет. Я сам работаю с основными данными для приложения, и я обнаружил, что есть несколько способов приблизиться к обработке основных данных, и с потоковой передачей это добавляет еще один уровень.
Как заявляет Apple в своей документации, вы не должны передавать NSManagedObjectContext между потоками из-за их внутренней функциональности. И по умолчанию все обновления пользовательского интерфейса должны выполняться в основном потоке. Итак, Я бы посоветовал вам сделать сохранение в основном потоке после того, как вы извлекли данные с помощью метода фонового потока. Как правило, я бы попытался следовать этому, но я не знаю, требует ли ваш проект фоновое сохранение?
Проблема возникает из-за того, что вы создаете экземпляр контейнера в фоновом потоке. Однако, когда он объявлен как static
в делегате приложения, происходит только одна инициализация, и он не инициализируется в фоновом потоке, что мешает его использованию.
Из API Apple для NSManagedObjectContext Веб-сайт Apple API:
Core Data uses thread (or serialized queue) confinement to protect managed objects and managed object contexts (see Core Data Programming Guide). A consequence of this is that a context assumes the default owner is the thread or queue that allocated it—this is determined by the thread that calls its init method. You should not, therefore, initialize a context on one thread then pass it to a different thread. Instead, you should pass a reference to a persistent store coordinator and have the receiving thread/queue create a new context derived from that. If you use Operation, you must create the context in main (for a serial queue) or start (for a concurrent queue).
Не инициализируйте и не настраивайте свой основной стек данных в делегате приложения. Используйте подкласс NSObject и сохраните его в качестве основного стека данных (код из учебника Рэя Вендерлиха. Учебник Рэя Вендерлиха (1 год)). Если используется, вы должны инициализировать это в делегате приложения, а затем передать его. Но помните, что ваша проблема возникает из-за потоковой передачи, поэтому вам нужно использовать статическую переменную, как вы это делали, или более рекомендуемый способ, сохранить в основные данные после завершения выборки и выхода из фонового потока .:
class CoreDataStack: NSObject {
let moduleName = "YourModuleName"
func saveToMainContext() { // Just a helper method for removing boilerplate code when you want to save. Remember this will be done on the main thread if called.
if objectContext.hasChanges {
do {
try objectContext.save()
} catch {
print("Error saving main ManagedObjectContext: \(error)")
}
}
}
lazy var managedObjectModel: NSManagedObjectModel = {
let modelURL = Bundle.main.url(forResource: moduleName, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
lazy var applicationDocumentsDirectory: URL = {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
}()
lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = {
let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel)
let persistenStoreURL = self.applicationDocumentsDirectory.appendingPathComponent("\(moduleName).sqlite")
do {
try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: persistenStoreURL, options: [NSMigratePersistentStoresAutomaticallyOption: true, NSInferMappingModelAutomaticallyOption : true])
} catch {
fatalError("Persistent Store error: \(error)")
}
return coordinator
}()
lazy var objectContext: NSManagedObjectContext = {
let context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) // As stated in the documentation change this depending on your need, but i recommend sticking to main thread if possible.
context.persistentStoreCoordinator = self.persistentStoreCoordinator
return context
}()
}
Использование делегата приложения в качестве настройки. Я обычно инициализирую объекты из делегата приложения с помощью (UIApplication.shared.delegate as! AppDelegate).persistentContainer
, когда они не статичны, и я должен инициализировать их оттуда, что будет ссылаться на текущий используемый делегат приложения. Однако это могло не иметь значения.
static
вместо делегата приложения.Надеюсь, я не опоздаю на это. Возможно, в противном случае это помогает кому-то другому. Удачи.