Skip to content

Instantly share code, notes, and snippets.

@levibostian
Last active October 6, 2023 18:02
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save levibostian/a7d46afec7e5cd72eadaadb2dcf7a227 to your computer and use it in GitHub Desktop.
Save levibostian/a7d46afec7e5cd72eadaadb2dcf7a227 to your computer and use it in GitHub Desktop.
iOS CoreData ultimate document. Stack, tutorial, errors could encounter. All my CoreData experience for what works in 1 document.

Official CoreData programming guide

Best practices

Most object relationships are inherently bidirectional. If a Department has a to-many relationship to the Employees who work in a Department, there is an inverse relationship from an Employee to the Department that is to-one. The major exception is a fetched property, which represents a weak one-way relationship—there is no relationship from the destination to the source. The recommended approach is to model relationships in both directions and specify the inverse relationships appropriately.

Deletion rules

When creating a deletion rule for a relationship always ask yourself, "What should happen when this model instance is deleted? What should happen to the model on the other end of the relationship?"

Example: You have an employee entity and department entity. In the employee entity you define a one-to-one relationship called "department" that maps to the department entity. In the department entity you define a one-to-many relationship called "employees" to the employee entity. You setup both of these 2 relationships to be the inverse of each other. The delete rule that you setup on "employees" is what will happen when the department entity instance is deleted. What will happen to the employee model instances in that relationship and their links. The delete rule on "department" is what will happen when the employee entity is deleted. What will happen to the department model instance in that relationship and it's link.

Deny

If there is at least one object at the relationship destination (employees), do not delete the source object (department).

For example, if you want to remove a department, you must ensure that all the employees in that department are first transferred elsewhere; otherwise, the department cannot be deleted.

Nullify

Remove the relationship (link) between the objects, but do not delete either object.

This only makes sense if the department relationship for an employee is optional, or if you ensure that you set a new department for each of the employees before the next save operation.

Cascade

Delete the objects at the destination of the relationship when you delete the source.

For example, if you delete a department, fire all the employees in that department at the same time.

No Action

Do nothing to the object at the destination of the relationship.

For example, if you delete a department, leave all the employees as they are, even if they still believe they belong to that department.

My CoreData stack

The pieces of it:

  1. The .xcdatamodelid file
  2. Setup the stack for the app
  3. Manually defining the Swift model classes
  4. Inserting, deleting, fetching.
  5. Unit testing with CoreData.

The .xcdatamodelid file

  • Taget membership: Your app
  • Entities inside:
    • Entity name: Name of your entity
    • Class name: Name of the Swift class that represents this model. I usually copy/paste from the entity name to prevent typos.
    • Module: Current Product Module
    • Codegen: Manual/none

Setup the stack for the app

This revolves around a file, AppCoreDataManager. The file is attached to this gist. We need to create a protocol, CoreDataManager because we will create a separate stack for testing later on.

The AppCoreDataManager is a singleton. All we need to do is load it when the app starts:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        AppCoreDataManager.shared.loadStore { error in 
            if let error = error {
                // log the error to crash reporting tool if you use one. 
                fatalError("CoreData load error \(error.localizedDescription)")
            }
            // load your ViewController. 
        }
        
        return true 
    }
    ...
}

Manually defining the Swift model classes

import CoreData
import Foundation

// @objc(CycleModel) // Not needed. Some stackoverflow answers said to use this to solve some problems but I have not needed to use it. 
class CycleModel: NSManagedObject, CycleContract {
    @NSManaged var cycleNumberRaw: Int16
    @NSManaged var quarterNumberRaw: Int16
    @NSManaged var ignored: Bool
    @NSManaged var started: Date?
    @NSManaged var ended: Date?

    var cycleNumber: Int {
        Int(cycleNumberRaw)
    }

    var quarterNumber: Int {
        Int(quarterNumberRaw)
    }

    @NSManaged var tasks: [ToDoListTaskModel]? // Model: on delete, deny. For safety reasons, we will only allow a cycle to be deleted if all of the tasks associated with that task are either moved or deleted. This prevents tasks from being deleted by accident.
    
    @nonobjc class func fetchRequest() -> NSFetchRequest<CycleModel> {
        NSFetchRequest<CycleModel>(entityName: name)
    }
}

extension CycleModel {
    static var name: String {
        "CycleModel"
    }

    static func insertFrom(_ vo: CycleVo, context: NSManagedObjectContext) -> CycleModel {
        // Using `NSEntityDescription.entity()` for inserting the model instance works. But, I am using `NSEntityDescription.insertNewObject()` because the official Apple CoreData document uses that. 
//        let model = CycleModel(entity: NSEntityDescription.entity(forEntityName: CycleModel.name, in: context)!, insertInto: context)
        let model = NSEntityDescription.insertNewObject(forEntityName: name, into: context) as! CycleModel // swiftlint:disable:this force_cast

        model.cycleNumberRaw = Int16(vo.cycleNumber)
        model.quarterNumberRaw = Int16(vo.quarterNumber)
        model.ignored = vo.ignored
        model.started = vo.started
        model.ended = vo.ended
        model.tasks = nil

        return model
    }

    func toVo() -> CycleVo {
        CycleVo(cycleNumber: cycleNumber, quarterNumber: quarterNumber, ignored: ignored, started: started, ended: ended)
    }
}

Notice that I used "CycleModel" for the static entity name. Some Stackoverflow answers suggest that you use the name of your module as a prefix. Example: "ModuleName.CycleModel". You may need to do this depending on how your project is setup but I found that this did not work for me and leaving it out worked.

I like to use VO struct objects to be deserialized from JSON network calls and displaying in the UI. I like to only use models for the saving to the DB layer and that's it. The less I need to deal with CoreData the less chances of errors.

To finish my example, see the attached file to this gist.

Inserting, deleting, fetching.

Inserting
coreDataManager.performBackgroundTask { context in
    _ = CycleModel.insertFrom(CycleVo(cycleNumber: 1, quarterNumber: 1, ignored: false, started: Date.yesterday, ended: nil), context: context)

   try! context.save()
}
Deleting
coreDataManager.performBackgroundTask { context in
    let request: NSFetchRequest<CycleModel> = CycleModel.fetchRequest()
    let cycle = try! context.fetch(request)[0]

    context.delete(cycle)

    try! context.save()
}
fetching
let request: NSFetchRequest<CycleModel> = CycleModel.fetchRequest()
let cycles = try! coreDataManager.uiContext.fetch(request)

Unit testing with CoreData.

First, we need to make sure that we do not run the App's CoreData stack when tests run. That means that we need to make sure that the AppCoreDataManager.loadStore {} does not run when we run tests.

I like to add this to my AppDelegate:

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // We must allow running a host app for unit tests so that we can test keychains in our unit tests. So to prevent the appdelegate from running any code, return early and have tests run assuming appdelegate code did not run.
        // See: https://github.com/kishikawakatsumi/KeychainAccess/issues/399
        if CommandLine.arguments.contains("--unit-testing") {
            return true
        }
        ...
    }
}

Then, I edit my scheme for running tests in XCode. Under Test > Arguments > add an argument --unit-testing. Now your AppDelegate code will not execute.

To unit test, you need a separate CoreData stack. One that is super fast and can erase for us between tests. We want to use in-memory. See the file TestCoreDataManager in this gist for the full file. Add this file to your tests and have it's target membership just be for tests.

To use it, it's quite simple.

import Foundation
import XCTest
import CoreData
@testable import App

class CycleModelTest: XCTestCase {

    var coreDataManager: TestCoreDataManager!

    override func setUp() {
        coreDataManager = TestCoreDataManager()

        super.setUp()
    }
    
    func test_delete_givenNoTasksForCycle_expectDeleteToSucceed() {
        let expectInsert = expectation(description: "Expect to insert")
        coreDataManager.performBackgroundTask { context in
            _ = CycleModel.insertFrom(CycleVo(cycleNumber: 1, quarterNumber: 1, ignored: false, started: Date.yesterday, ended: nil), context: context)

            try! context.save()

            expectInsert.fulfill()
        }
        waitForExpectations(for: [expectInsert])

        let expectToDelete = expectation(description: "Expect to delete")
        coreDataManager.performBackgroundTask { context in
            let request: NSFetchRequest<CycleModel> = CycleModel.fetchRequest()
            let cycle = try! context.fetch(request)[0]

            context.delete(cycle)

            try! context.save()

            expectToDelete.fulfill()
        }
        waitForExpectations(for: [expectToDelete])
        
        let request: NSFetchRequest<CycleModel> = CycleModel.fetchRequest()
        let cycles = try! coreDataManager.unsafeContext.fetch(request)

        XCTAssertTrue(cycles.isEmpty)
    }    
}

Errors you may encounter

'executeFetchRequest:error: <null> is not a valid NSFetchRequest.'

This error was being thrown when I was trying to perform a fetch request: let cycles = try! coreDataManager.uiContext.fetch(CycleModel.fetchRequest()) However, I found that the error message is not actually the error message we should be paying attention to. In XCode, apply a filter in the console for "coredata" to only show log messages for CoreData. You will see errors being thrown which is the root problem that is causing the error we are talking about here.

In the XCode console, I found the error Multiple NSEntityDescriptions claim the NSManagedObject subclass. This is the error we need to fix.

There may be a combination of different problems that cause this error to happen. However, I will share what I did to solve it for me. (1) read the rest of this document to learn about my CoreData stack. If you compare it to yours it may help find other problems and (2) read my fixes below.

The error of Multiple NSEntityDescriptions claim the NSManagedObject subclass was happening when I was performing fetches in my test function: let cycles = try! coreDataManager.uiContext.fetch(CycleModel.fetchRequest()) I figured I am fetching incorrectly then. I felt it was worth the try to try alternatives to filtering. After reading the official Apple docs on fetching in CoreData, I made the following changes:

  1. I added this function to my NSManagedObject subclass:
class CycleModel: NSManagedObject {
    ...
    
    @nonobjc class func fetchRequest() -> NSFetchRequest<CycleModel> {
        NSFetchRequest<CycleModel>(entityName: "CycleModel")
    }
}
  1. I modified my fetch code in my tests.
let request: NSFetchRequest<CycleModel> = CycleModel.fetchRequest()
let cycles = try! coreDataManager.uiContext.fetch(request)

The CoreData API provides to you a default NSManagedObject.fetchRequest() function which is why I am providing the type for request strictly so that I know the compiler will use the fetchRequest() I made and not the default API provided one.

[error] warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'ModelName' so +entity is unable to disambiguate. or, [error] warning: 'ModelName' (0x7f986861cae0) from NSManagedObjectModel (0x7f9868604090) claims 'ModelName'. or, Failed to find a unique match for an NSEntityDescription to a managed object subclass at

Happened because part of the CoreData stack was being loaded multiple times while testing. You know this because if you run the test functions one at a time, they all pass without this error. It only happens if you run 2+ tests that use the CoreData stack at a time.

I used this SO answer to help me fix this problem. If you make the NSManagedObjectModel a singleton so it's only loaded once per all tests. You can load NSPersistentContainer in the setUp() of your test class. In fact, I think loading a new NSPersistentContainer for each test function is what clears the database for you as it's in-memory!

Modify your CoreData stack to make this a singleton.

class TestCoreDataManager: CoreDataManager {
        
    static var sharedMom: NSManagedObjectModel = {
        let modelURL = Bundle(for: AppCoreDataManager.self).url(forResource: AppCoreDataManager.nameOfModelFile, withExtension: "momd")!
        return NSManagedObjectModel(contentsOf: modelURL)!
    }()
    
    let persistentContainerQueue = OperationQueue() // make a queue for performing write operations to avoid conflicts with multiple writes at the same time from different contexts.
    static let nameOfModelFile = AppCoreDataManager.nameOfModelFile
    private let threadUtil: ThreadUtil = AppThreadUtil()
    
    private let persistentContainer: NSPersistentContainer

    init() {
        let persistentStoreDescription = NSPersistentStoreDescription()
        persistentStoreDescription.type = NSInMemoryStoreType
            
        let container = NSPersistentContainer(name: AppCoreDataManager.nameOfModelFile, managedObjectModel: TestCoreDataManager.sharedMom)

        container.persistentStoreDescriptions = [persistentStoreDescription]
        
        container.loadPersistentStores { _, error in
            if let error = error as NSError? {
            fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        }
        
        self.persistentContainer = container
    }
    
    ...
}

There are some other SO questions 1 and many of them share the same pattern where the error only happens for unit tests and incorporating singletons fixes the issue. However, some of those people's CoreData stack is different then mine so it was difficult to use those answers.

This SO answer also helped improve my use of CoreData. I made breakpoints in my test code to see what line of code made this error show up. I found that this error was only happening when inserting new models into the DB. From that SO answer, I modified my insert code from: let model = CycleModel(context: context) to let model = CycleModel(entity: NSEntityDescription.entity(forEntityName: CycleModel.name, in: context)!, insertInto: context). I guess using NSEntityDescription is more explicit and lowers the chance of errors from happening in your app. After doing this 1 line of code change, I encountered a new error: executeFetchRequest:error: A fetch request must have an entity. Read about this error in this document. I outline it in here.

executeFetchRequest:error: A fetch request must have an entity.

Help from this SO answer fixed that problem right away. I added this code:

sharedMom.entities.forEach { (description) in
    print("[COREDATA] entity: \(description.name!)")
}

self.persistentContainer.managedObjectModel.entities.forEach { (des) in
    print("[COREDATA] container entity: \(des.name!)")
}

to my test CoreData stack after creating the persistentContainer instance. It was at this point that I realized I had a typo in my entity name in the .xcdatamodelid file in XCode...I edited the entity name and that error was fixed.

Unable to find specific subclass of NSManagedObject

This is usually part of your CoreData setup. In your .xcdatamodelid file in XCode you may have entered in the wrong model name, the wrong module, etc. It is usually a quick fix. This SO question is good inspiration to a few ways you can fix this.

import CoreData
import Foundation
protocol CoreDataManager {
var uiContext: NSManagedObjectContext { get }
func newBackgroundContext() -> NSManagedObjectContext
func performBackgroundTaskOnUI(_ block: @escaping (NSManagedObjectContext) -> Void)
func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void)
func loadStore(completionHandler: ((Error?) -> Void)?)
}
// File inspired from UseYourLoaf CoreDataController: https://gist.github.com/kharrison/0d19af0729ae324b8243a738844f8245
class AppCoreDataManager: CoreDataManager {
public static var shared: AppCoreDataManager = AppCoreDataManager()
let persistentContainerQueue = OperationQueue() // make a queue for performing write operations to avoid conflicts with multiple writes at the same time from different contexts.
static let nameOfModelFile = "Model" // The name of the .xcdatamodeld file you want to use. Here, I have a file `Model.xcdatamodeld` in XCode.
private let threadUtil: ThreadUtil = AppThreadUtil()
// Note: Must call `loadStore()` to initialize. Do not forget to do that.
init() {
persistentContainerQueue.maxConcurrentOperationCount = 1
if let storeURL = self.storeURL {
let description = storeDescription(with: storeURL)
persistentContainer.persistentStoreDescriptions = [description]
}
}
/// The managed object context associated with the main queue (read-only).
/// To perform tasks on a private background queue see
/// `performBackgroundTask:` and `newPrivateContext`.
var uiContext: NSManagedObjectContext {
threadUtil.assertMain()
return persistentContainer.viewContext
}
/// Create and return a new private queue `NSManagedObjectContext`. The
/// new context is set to consume `NSManagedObjectContextSave` broadcasts
/// automatically.
///
/// Use this instead of the UI thread context when you want read-only background thread operations.
///
/// - Returns: A new private managed object context
public func newBackgroundContext() -> NSManagedObjectContext {
persistentContainer.newBackgroundContext()
}
// Perform write operation on UI thread.
func performBackgroundTaskOnUI(_ block: @escaping (NSManagedObjectContext) -> Void) {
threadUtil.assertMain()
persistentContainerQueue.addOperation {
self.persistentContainer.performBackgroundTask { context in
block(context)
// We used to have `try! context.save()` here to automatically save. But, we shouldn't because saving can fail. So, require the caller of this function to handle when an error happens.
}
}
}
// Perform write operations on background thread
func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
// threadUtil.assertBackground()
persistentContainerQueue.addOperation {
let context: NSManagedObjectContext = self.persistentContainer.newBackgroundContext()
context.performAndWait {
block(context)
// We used to have `try! context.save()` here to automatically save. But, we shouldn't because saving can fail. So, require the caller of this function to handle when an error happens.
}
}
}
/// The `URL` of the persistent store for this Core Data Stack. If there
/// is more than one store this property returns the first store it finds.
/// The store may not yet exist. It will be created at this URL by default
/// when first loaded.
var storeURL: URL? {
var url: URL?
let descriptions = persistentContainer.persistentStoreDescriptions
if let firstDescription = descriptions.first {
url = firstDescription.url
}
return url
}
/// Destroy a persistent store.
///
/// - Parameter storeURL: An `NSURL` for the persistent store to be
/// destroyed.
/// - Returns: A flag indicating if the operation was successful.
/// - Throws: If the store cannot be destroyed.
func destroyPersistentStore(at storeURL: URL) throws {
let psc = persistentContainer.persistentStoreCoordinator
try psc.destroyPersistentStore(at: storeURL, ofType: NSSQLiteStoreType, options: nil)
}
/// Replace a persistent store.
///
/// - Parameter destinationURL: An `NSURL` for the persistent store to be
/// replaced.
/// - Parameter sourceURL: An `NSURL` for the source persistent store.
/// - Returns: A flag indicating if the operation was successful.
/// - Throws: If the persistent store cannot be replaced.
func replacePersistentStore(at url: URL, withPersistentStoreFrom sourceURL: URL) throws {
let psc = persistentContainer.persistentStoreCoordinator
try psc.replacePersistentStore(at: url, destinationOptions: nil,
withPersistentStoreFrom: sourceURL, sourceOptions: nil, ofType: NSSQLiteStoreType)
}
/// A read-only flag indicating if the persistent store is loaded.
public private(set) var isStoreLoaded = false
// Note: Meant to be called from UI thread as completionHandler will be called to Ui thread.
func loadStore(completionHandler: ((Error?) -> Void)?) {
persistentContainer.loadPersistentStores { _, error in
if error == nil {
self.isStoreLoaded = true
self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
self.persistentContainer.viewContext.shouldDeleteInaccessibleFaults = true
} else {
if completionHandler == nil {
fatalError("Error: \(error!)")
}
}
DispatchQueue.main.async {
completionHandler?(error)
}
}
}
private lazy var persistentContainer: NSPersistentContainer = {
let bundle = Bundle(for: AppCoreDataManager.self)
let mom = NSManagedObjectModel.mergedModel(from: [bundle])!
return NSPersistentContainer(name: AppCoreDataManager.nameOfModelFile, managedObjectModel: mom)
}()
private func storeDescription(with url: URL) -> NSPersistentStoreDescription {
let description = NSPersistentStoreDescription(url: url)
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
description.shouldAddStoreAsynchronously = true
description.isReadOnly = false
description.type = NSSQLiteStoreType
return description
}
}
// This file adds onto the `CycleModel` class found in the README
protocol CycleContract: Equatable {
var cycleNumber: Int { get }
var quarterNumber: Int { get }
var ignored: Bool { get }
var started: Date? { get }
var ended: Date? { get }
}
struct CycleVo: CycleContract, Codable, AutoLenses, Equatable {
let cycleNumber: Int
let quarterNumber: Int
let ignored: Bool
let started: Date?
let ended: Date?
}
// Other model that Cycle uses
import CoreData
import Foundation
//@objc(ToDoListTaskModel) // comment this out to prevent error "Failed to find a unique match for NSEntityDescription to a managed object subclass
class ToDoListTaskModel: NSManagedObject, ToDoListTaskContract {
@NSManaged var name: String
@NSManaged var startDate: Date?
@NSManaged var endDate: Date?
@NSManaged var allDay: Bool
@NSManaged var completed: Bool
@NSManaged var ratingRaw: Int16 // -1 is default value to represent nil
@NSManaged var cycle: CycleModel // Model: on delete, nullify. Do not delete cycle if we delete this task as the cycle might have more tasks.
var rating: Int? {
guard ratingRaw >= 0 else {
return nil
}
return Int(ratingRaw)
}
@nonobjc class func fetchRequest() -> NSFetchRequest<ToDoListTaskModel> {
NSFetchRequest<ToDoListTaskModel>(entityName: name)
}
}
extension ToDoListTaskModel {
static var name: String {
"ToDoListTaskModel"
}
static func insertFrom(_ vo: ToDoListTaskVo, cycle: CycleModel, context: NSManagedObjectContext) -> ToDoListTaskModel {
// let model = ToDoListTaskModel(entity: NSEntityDescription.entity(forEntityName: ToDoListTaskModel.name, in: context)!, insertInto: context)
let model = NSEntityDescription.insertNewObject(forEntityName: name, into: context) as! ToDoListTaskModel // swiftlint:disable:this force_cast
model.name = vo.name
model.startDate = vo.startDate
model.endDate = vo.endDate
model.allDay = vo.allDay
model.completed = vo.completed
model.cycle = cycle
if let rating = vo.rating {
model.ratingRaw = Int16(rating)
} else {
model.ratingRaw = -1
}
return model
}
func toVo() -> ToDoListTaskVo {
ToDoListTaskVo(name: name, startDate: startDate, endDate: endDate, allDay: allDay, completed: completed, rating: rating)
}
}
protocol ToDoListTaskContract: Equatable {
var name: String { get }
// null if task belongs to the "unsure dates" list
var startDate: Date? { get }
var endDate: Date? { get }
// ignore the time from startDate/endDate if true
var allDay: Bool { get }
var completed: Bool { get }
// rating a task is optional. Only populate if answered.
var rating: Int? { get }
}
struct ToDoListTaskVo: ToDoListTaskContract, Codable, AutoLenses, Equatable {
let name: String
let startDate: Date?
let endDate: Date?
let allDay: Bool
let completed: Bool
let rating: Int?
// specifies if the user can delete/edit this item. If the task has not yet happened, they can edit it.
var isEditable: Bool {
if completed {
return false
}
guard let startDate = self.startDate else {
return false
}
let todayEndOfDay = Date.today.endOfDay
return startDate > todayEndOfDay
}
static func allDay(name: String, startDate: Date, endDate: Date) -> ToDoListTaskVo {
ToDoListTaskVo(name: name, startDate: startDate, endDate: endDate, allDay: true, completed: false, rating: nil)
}
static func timeBased(name: String, startDate: Date, endDate: Date) -> ToDoListTaskVo {
ToDoListTaskVo(name: name, startDate: startDate, endDate: endDate, allDay: false, completed: false, rating: nil)
}
static func unsureDate(name: String) -> ToDoListTaskVo {
ToDoListTaskVo(name: name, startDate: nil, endDate: nil, allDay: false, completed: false, rating: nil)
}
}
import Foundation
import CoreData
@testable import App
class TestCoreDataManager: CoreDataManager {
static var sharedMom: NSManagedObjectModel = {
let modelURL = Bundle(for: AppCoreDataManager.self).url(forResource: AppCoreDataManager.nameOfModelFile, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
let persistentContainerQueue = OperationQueue() // make a queue for performing write operations to avoid conflicts with multiple writes at the same time from different contexts.
static let nameOfModelFile = AppCoreDataManager.nameOfModelFile
private let threadUtil: ThreadUtil = AppThreadUtil()
private let persistentContainer: NSPersistentContainer
init() {
let persistentStoreDescription = NSPersistentStoreDescription()
persistentStoreDescription.type = NSInMemoryStoreType
let container = NSPersistentContainer(name: AppCoreDataManager.nameOfModelFile, managedObjectModel: TestCoreDataManager.sharedMom)
container.persistentStoreDescriptions = [persistentStoreDescription]
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
self.persistentContainer = container
}
lazy var uiContext: NSManagedObjectContext = {
threadUtil.assertMain() // we are checking for main, just like in the App version of CoreDataManager because we want the app tests to behave as close to the App version as possible.
return persistentContainer.viewContext
}()
// Mostly used for testing. *unsafe* means that we are not checking what thread you are on.
lazy var unsafeContext: NSManagedObjectContext = {
persistentContainer.viewContext
}()
func newBackgroundContext() -> NSManagedObjectContext {
persistentContainer.newBackgroundContext()
}
func performBackgroundTaskOnUI(_ block: @escaping (NSManagedObjectContext) -> Void) {
threadUtil.assertMain()
persistentContainerQueue.addOperation {
self.persistentContainer.performBackgroundTask { context in
block(context)
// We used to have `try! context.save()` here to automatically save. But, we shouldn't because saving can fail. So, require the caller of this function to handle when an error happens.
}
}
}
func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
// threadUtil.assertBackground()
persistentContainerQueue.addOperation {
let context: NSManagedObjectContext = self.persistentContainer.newBackgroundContext()
context.performAndWait {
block(context)
// We used to have `try! context.save()` here to automatically save. But, we shouldn't because saving can fail. So, require the caller of this function to handle when an error happens.
}
}
}
func loadStore(completionHandler: ((Error?) -> Void)?) {
// don't do anything. We load the store on init for in-memory stores.
completionHandler?(nil)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment