Правильный способ работы с экземпляром persistentContainer в AppDelegate

Когда мы создаем новый проект в 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?

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
4
0
4 139
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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).

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

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

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

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

Другие вопросы по теме