在 AppDelegate 上处理persistentContainer 实例的正确方法

Mar*_*aka 4 core-data ios swift

当我们在选中 Core Data 选项的情况下在 Xcode 中创建一个新项目时,它会在 AppDelegate.swift 上生成一个定义 Core Data 堆栈的新项目:

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)")
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

为了方便我访问persistentContainer,我还添加了这段代码:

static var persistentContainer: NSPersistentContainer {
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { fatalError("Could not convert delegate to AppDelegate") }
    return appDelegate.persistentContainer
}
Run Code Online (Sandbox Code Playgroud)

所以我可以这样称呼它:

let container = AppDelegate.persistentContainer
Run Code Online (Sandbox Code Playgroud)

当我尝试从后台线程访问它时会出现问题。例如,我有一段代码在后台运行并从 Web 服务中获取一些数据。当它获取数据时,我使用以下方法保存它:

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...
        })
    }
}
Run Code Online (Sandbox Code Playgroud)

当我尝试获取持久容器时,它会在控制台上生成:

Main Thread Checker: UI API called on a background thread: -[UIApplication delegate]
Run Code Online (Sandbox Code Playgroud)

为了不再发生这种情况,我已将我persistentContainerlazy var改为staticon AppDelegate

static var persistentContainer: NSPersistentContainer = {
    // same code as before...
}()
Run Code Online (Sandbox Code Playgroud)

并且错误不再发生。

但我想知道这是否会产生任何我不知道的副作用。我的意思是,persistentContainer无论如何我都只有一个,因为只有一个实例AppDelegate对吗?所以我可以像我一样将它更改为静态并AppDelegate.persistentContainer在我的应用程序的其他部分使用它来访问它而没有任何问题?

或者是否有另一种推荐的模式来处理persistentContainer实例化和使用?

the*_*ahl 7

介绍

你好。我自己正在处理应用程序的核心数据,我发现有几种方法可以处理核心数据,并且通过线程处理它增加了另一层。

正如 Apple 在其文档中所述,由于其内部功能,您不应在线程之间传递 NSManagedObjectContext。默认情况下,所有 UI 更新都应在主线程上完成。因此,我建议您在使用后台线程方法获取数据后,在主线程上进行保存。作为一般规则,我会尝试遵循这一点,但我不知道您的项目是否需要后台保存?

问题病因

问题来自您在后台线程中实例化容器。但是,当它static在应用程序委托中声明时,只会发生一次初始化,并且不会在后台线程上初始化,这会干扰其使用。

来自 Apple API for NSManagedObjectContext Apple API 网站

Core Data 使用线程(或序列化队列)限制来保护托管对象和托管对象上下文(请参阅 Core Data 编程指南)。这样做的结果是,上下文假定默认所有者是分配它的线程或队列——这由调用其 init 方法的线程确定。因此,您不应在一个线程上初始化上下文,然后将其传递给另一个线程。相反,您应该传递对持久存储协调器的引用,并让接收线程/队列创建一个从中派生的新上下文。如果使用 Operation,则必须在 main(对于串行队列)或 start(对于并发队列)中创建上下文。

初始化核心数据栈的方法

  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
        }()
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用应用程序委托作为设置。我通常从应用程序委托初始化对象,(UIApplication.shared.delegate as! AppDelegate).persistentContainer当它们不是静态时,我必须从那里初始化,这将引用当前使用的应用程序委托。然而,这可能无关紧要。

    • 或者,您可以static在应用程序委托中使用。

希望我不会为此迟到。也许它可以帮助其他人。祝你好运。