5

I want to start learning to use NSAsynchronousFetchRequest by referring to https://www.marcosantadev.com/coredata_crud_concurrency_swift_2/

I have the simplest use case of NSAsynchronousFetchRequest

NSAsynchronousFetchRequest

// Call from UI main thread
func X() {
    let fetchRequest = NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote")
    
    let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { asynchronousFetchResult in
        guard let result = asynchronousFetchResult.finalResult as? [NSPlainNote] else { return }
    }

    let coreDataStack = CoreDataStack.INSTANCE
    // backgroundContext created via persistentContainer.newBackgroundContext()
    let backgroundContext = coreDataStack.backgroundContext
    
    backgroundContext.perform {
        do {
            try backgroundContext.execute(asynchronousFetchRequest)
        } catch let error {
            backgroundContext.rollback()
            
            error_log(error)
        }
    }
}

However, running the above code will get me the following error

CoreData`+[NSManagedObjectContext Multithreading_Violation_AllThatIsLeftToUsIsHonor]:

If I modify the code by using NSFetchRequest directly.

NSFetchRequest

// Call from UI main thread
func X() {
    let fetchRequest = NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote")

    let coreDataStack = CoreDataStack.INSTANCE
    // backgroundContext created via persistentContainer.newBackgroundContext()
    let backgroundContext = coreDataStack.backgroundContext
    
    backgroundContext.perform {
        do {
            let nsPlainNotes = try fetchRequest.execute()
        } catch let error {
            backgroundContext.rollback()
            
            error_log(error)
        }
    }
}

Thing works fine. May I know, what's wrong with my NSAsynchronousFetchRequest version of code?

This is my CoreDataStack.swift for reference purpose.

CoreDataStack.swift

import CoreData

class CoreDataStack {
    static let INSTANCE = CoreDataStack()
    
    private init() {
    }
    
    private(set) lazy var persistentContainer: NSPersistentContainer = {
        let container = NSPersistentContainer(name: "wenote")
        
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // This is a serious fatal error. We will just simply terminate the app, rather than using error_log.
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
        // So that when backgroundContext write to persistent store, container.viewContext will retrieve update from
        // persistent store.
        container.viewContext.automaticallyMergesChangesFromParent = true
        
        // TODO: Not sure these are required...
        //
        //container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        //container.viewContext.undoManager = nil
        //container.viewContext.shouldDeleteInaccessibleFaults = true
        
        return container
    }()
    
    private(set) lazy var backgroundContext: NSManagedObjectContext = {
        let backgroundContext = persistentContainer.newBackgroundContext()

        // Similar behavior as Android's Room OnConflictStrategy.REPLACE
        backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
        
        // TODO: Not sure these are required...
        //backgroundContext.undoManager = nil
        
        return backgroundContext
    }()
}

Additional information

Do note that, in NSAsynchronousFetchRequest example, even if backgroundContext.perform is not used.

// Call from UI main thread
func X() {
    let fetchRequest = NSFetchRequest<NSPlainNote>(entityName: "NSPlainNote")

    let asynchronousFetchRequest = NSAsynchronousFetchRequest(fetchRequest: fetchRequest) { asynchronousFetchResult in
        guard let result = asynchronousFetchResult.finalResult as? [NSPlainNote] else { return }
    }

    let coreDataStack = CoreDataStack.INSTANCE
    // backgroundContext created via persistentContainer.newBackgroundContext()
    let backgroundContext = coreDataStack.backgroundContext

    do {
        try backgroundContext.execute(asynchronousFetchRequest)
    } catch let error {
        backgroundContext.rollback()

        error_log(error)
    }
}

Same fatal error still occur.

Please note that, this fatal error will only be triggered, by editing the schema with Arguments Passed On Launch

-com.apple.CoreData.ConcurrencyDebug 1

enter image description here

I even try to execute some simple project from https://github.com/abhishekbedi1432/Core-Data-Asynchronous-Fetching/tree/master which is using NSAsynchronousFetchRequest.

If I do not enable -com.apple.CoreData.ConcurrencyDebug 1, the sample project from github able to perform asynchronous fetch without issue. However, once the -com.apple.CoreData.ConcurrencyDebug 1 is enabled, it will also be getting the same fatal error.

Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • Try replacing your lazy backgroundContext variable with directly calling newBackgroundContext() so you get a new context each time – Joakim Danielson Dec 31 '21 at 08:27
  • Yes. Try that. Same fatal error occurs. I have added more useful findings under "Additional information"... – Cheok Yan Cheng Dec 31 '21 at 08:51
  • @CheokYanCheng Please see my answer on your other question – it also touches this subject https://stackoverflow.com/a/70637770/921573 – de. Jan 10 '22 at 16:30

2 Answers2

0

You have to make a new context either a child or a root parent for present Core Data container, like so:

let backgroundContext = persistentContainer.newBackgroundContext()
backgroundContext.parent = persistentContainer.viewContext

Exhaustive explanation of using multiple contexts is here.

Oschły
  • 53
  • 1
  • 7
  • 1
    I don't think that's right. The `newBackgroundContext()` method gives a context that already has a parent. If you try to set a new one, it throws an internal consistency error. – covfefesurprise Jan 01 '22 at 21:56
  • Oh, you're right. The only part I had different was a core data stack (I've used the one provided by Xcode), but changing it to the OP's implementation throws an exception at the assigning new parent code. After digging a little in attached gh repo's code, it looks like NSAsynchronousFetchRequest just doesn't work on contexts that are set to use private queue, as with the ones set to main queue it works as expected (viewContext and one initialised manually and assigned to the container). – Oschły Jan 02 '22 at 08:39
0

For me, the async fetch request works as expected when I provide an estimatedResultCount value to it.

roanutil
  • 3
  • 2