swiftcore-datansmanagedobjectnsmanagedobjectcontext

Core Data where do I need to call save method


I am using core data and I am confused about NSManagedObjectContext. I used following code to save my book data. Book data is saving in my one of my application extension side like following. Extension side code block is calling in different than main thread like in the image. enter image description here

I am wondering does it cause a problem because of misusing of my contexts?

func saveBook() {
    let book: Book = Book(context: viewContext)
    let uuid = UUID()
    book.sessionId = uuid
    book.appId = session.appId
    book.startedAt = session.startedAt
    book.createdBy = AppData.installId

    coreData.saveSync()
}

func saveSync() {
        let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        privateContext.parent = viewContext
        privateContext.perform {
            do {
                try privateContext.save()
                viewContext.performAndWait {
                    do  {
                        try viewContext.save()
                    } catch {
                        let nsError = error as NSError
                        print("Unresolved error \(nsError), \(nsError.userInfo)")
                    }
                }
            } catch {
                let nsError = error as NSError
                print("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }

Solution

  • I see two problems in your code:

    1. The privateContext in the saveSync function is ponintless, as the new Book object was created in the mainContext. So, when you save the privateContext you are saving an empty context that has no changes.

    2. If I understand well, your saveBook() function is being executed in a background thread, and you are using the mainContext, which is always associated with the main thread.

    When dealing with Core Data in a multi-thread environment, you need to remember the following rule: Never use an NSManagedObjectContext or an NSManagedObject in a queue/thread other than the one assigned to your context, or you get unexpected results and your app will crash at some point.

    So, to fix both issues, you can do the following:

    func saveBook() {
        let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
        privateContext.parent = viewContext
        privateContext.perform {
            let book: Book = Book(context: privateContext)
            let uuid = UUID()
            book.sessionId = uuid
            book.appId = session.appId
            book.startedAt = session.startedAt
            book.createdBy = AppData.installId
            
            do {
                try privateContext.save()
                viewContext.performAndWait {
                    do  {
                        try viewContext.save()
                    } catch {
                        let nsError = error as NSError
                        print("Unresolved error \(nsError), \(nsError.userInfo)")
                    }
                }
            } catch {
                let nsError = error as NSError
                print("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
    

    Notice that, when you save the privateContext, the data are not persisted to the persistent store but are "saved" to its parent context. And it will only be persisted when the mainContext is saved. That's why you need to save both contexts after the Book object is created.

    If you have the persistentStoreContainer handy in your class or structure, you can use the following approach to create a background context and save it:

    func saveBook() {
        let privateContext = persistentStoreContainer.newBackgroundContext()
        privateContext.perform {
            let book: Book = Book(context: privateContext)
            let uuid = UUID()
            book.sessionId = uuid
            book.appId = session.appId
            book.startedAt = session.startedAt
            book.createdBy = AppData.installId
    
            do {
                try privateContext.save()
            } catch {
                let nsError = error as NSError
                print("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }
    

    The benefit of this approach is that the data is persisted when you save the privateContext, since its parent is the persistentStoreCoordinator.

    Edit:

    Here's another approach for creating a background context from the persistent container:

    func saveBook() {
        persistentStoreContainer.performBackgroundTask { context in
            let book: Book = Book(context: privateContext)
            let uuid = UUID()
            book.sessionId = uuid
            book.appId = session.appId
            book.startedAt = session.startedAt
            book.createdBy = AppData.installId
    
            do {
                try context.save()
            } catch {
                let nsError = error as NSError
                print("Unresolved error \(nsError), \(nsError.userInfo)")
            }
        }
    }