iosswiftcore-datansmanagedobjectnsmanagedobjectcontext

Passing ManagedObject collection from fetch result to closure up to ViewController


I want to perform a background fetch and pass the result to closure. Currently I'm using performBackgroundTask method from NSPersistentContainer which is giving a NSManagedObjectContext as a closure. Then using that context I'm executing fetch request. When fetch is done I'm, passing the result to the completion handler.

func getAllWorkspacesAsync(completion: @escaping ([Workspace]) -> Void) {
    CoreDataStack.shared.databaseContainer.performBackgroundTask { childContext in
        let workspacesFetchRequest: NSFetchRequest<Workspace> = NSFetchRequest(entityName: "Workspace")
        workspacesFetchRequest.predicate = NSPredicate(format: "remoteId == %@", "\(UserDefaults.lastSelectedWorkspaceId))")
        do {
            let workspaces: [Workspace] = try childContext.fetch(workspacesFetchRequest)
            completion(workspaces)
        } catch let error as NSError {
            // Handle error
        }
    }
}

I'm going to call this method from ViewModel and use Combine PassthroughSubject to notify the ViewController about the event.

class WorkspaceViewModel {

    private var cancellables: Set<AnyCancellable> = []
    let resultPassthroughObject: PassthroughSubject<[Workspace], Error> = PassthroughSubject()

    private let cacheManager = WorkspaceCacheProvider.shared

    static public let shared: WorkspaceViewModel = {
        let instance = WorkspaceViewModel()
        return instance
    }()

    func fetchWorkspaces() {
        cacheManager.getAllWorkspacesAsync { [weak self] workspaces in
            guard let self = self else { return }
            self.resultPassthroughObject.send(workspaces)
        }
    }
}

And the ViewController code:

class WorkspaceViewController: UIViewController {

    private var cancellables: Set<AnyCancellable> = []

    override func viewDidLoad() {
        super.viewDidLoad()
    
        WorkspaceViewModel.shared.resultPassthroughObject
            .receive(on: RunLoop.main)
            .sink { _ in } receiveValue: { workspaces in
            // update the UI
        }
        .store(in: &cancellables)

    }
}

My question is: is it safe to pass NSManagedObject items?


Solution

  • No, it is not

    Do not pass NSManagedObject instances between queues. Doing so can result in corruption of the data and termination of the app. When it is necessary to hand off a managed object reference from one queue to another, use NSManagedObjectID instances.

    You would want something like:

    func getAllWorkspacesAsync(completion: @escaping ([NSManagedObjectID]) -> Void) {
        CoreDataStack.shared.databaseContainer.performBackgroundTask { childContext in
            let workspacesFetchRequest: NSFetchRequest<Workspace> = NSFetchRequest(entityName: "Workspace")
            workspacesFetchRequest.predicate = NSPredicate(format: "remoteId == %@", "\(UserDefaults.lastSelectedWorkspaceId))")
            do {
                let workspaces: [Workspace] = try childContext.fetch(workspacesFetchRequest)
                completion(workspaces.map { $0.objectID } )
            } catch let error as NSError {
                // Handle error
            }
        }
    }
    

    You will need to make corresponding changes to your publisher so that it publishes [NSManagedObjectID].

    Once you have received this array in your view controller you will need to call object(with:) on your view context to get the Workspace itself. This will perform a fetch in the view context for the object anyway.

    You may want to consider whether the time taken to fetch your workspaces warrants the use of a background context. By default Core Data objects are retrieved as faults and the actual attribute values are only fetched when the fault is triggered by accessing an object's attributes.

    Alternatively, if the purpose of the view controller is to present a list of workspaces that the user can select from, you could return an array of tuples or structs containing the workspace name and the object id. However, this all sounds like pre-optimisation to me.