Правильный способ работы с экземпляром persistentContainer в AppDelegate - PullRequest
0 голосов
/ 09 сентября 2018

Когда мы создаем новый проект в XCode с выбранной опцией 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 создания и использования?

Ответы [ 2 ]

0 голосов
/ 06 декабря 2018

Введение

Hello. Я сам работаю с основными данными для приложения и обнаружил, что есть несколько способов приблизиться к обработке основных данных, и с многопоточностью это добавляет еще один слой.

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

Вопросы этиологии

Проблема возникает из-за того, что вы создаете экземпляр контейнера в фоновом потоке. Однако когда он объявлен как static в делегате приложения, происходит только одна инициализация, и она не инициализируется в фоновом потоке, что мешает его использованию.

Из API Apple для NSManagedObjectContext Веб-сайт Apple API :

Базовые данные используют ограничение потока (или сериализованной очереди) для защиты управляемые объекты и контексты управляемых объектов (см. Базовое программирование данных Руководство). Следствием этого является то, что контекст предполагает, что по умолчанию владелец - это поток или очередь, который его выделил - это определяется поток, который вызывает его метод init. Поэтому вы не должны инициализировать контекст в одном потоке, а затем передать его в другой поток. Вместо этого вы должны передать ссылку на постоянный координатор магазина и получающий поток / очередь создает новый контекст, полученный из тот. Если вы используете Operation, вы должны создать контекст в main (для последовательная очередь) или начало (для параллельной очереди).

Подходы для инициализации основного стека данных

  1. Не инициализируйте и не настраивайте свой основной стек данных в делегате приложения. Используйте подкласс NSObject и сохраните его в качестве основного стека данных (Код из руководства Ray Wenderlich. Руководство Ray Wenderlich (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
        }()
    }
    
  2. Использование делегата приложения в качестве настройки. Обычно я инициализирую объекты из делегата приложения с помощью (UIApplication.shared.delegate as! AppDelegate).persistentContainer, когда они не являются статичными, и мне нужно инициализировать их оттуда, что будет ссылаться на текущий используемый делегат приложения. Однако это может не иметь значения.

    • В качестве альтернативы вы можете использовать static вместо этого в делегате приложения.

Надеюсь, я не опоздаю на это. Возможно, это помогает кому-то другому в противном случае. Удачи.

0 голосов
/ 09 сентября 2018

PersistentContainer работает в главной очереди. Как следует из названия свойства, этот контекст управляемого объекта предназначен для использования в сочетании с пользовательским интерфейсом приложения. Может быть, вам нужно отправить обратно в основную очередь для взаимодействия с UIApplicationDelegate и PersistentContainer, используя ...

DispatchQueue.main.async {

   // save data…

}

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...