danu
danu

Reputation: 1188

Fetch CoreData using Generic Model and privateQueueConcurrencyType

I've made a method to fetch my coredata Objects array using generics and privateQueueConcurrencyType. However, I'm doing something wrong somewhere and I'm experiencing crashes because of that.

The place where I get the crash as follow. what am I missing here ??

func fetchData<T: NSFetchRequestResult>(entity: String, model: T.Type, _ custom_predicate: NSPredicate?=nil) throws -> [T] {
   let request = NSFetchRequest<T>(entityName: entity)

    if custom_predicate != nil {
       request.predicate =   custom_predicate
    }

   request.returnsObjectsAsFaults = false
   //Crash is the bellow line
   return try privateMOC.fetch(request)//This line throws the crash

}

My privateMOC initialisation as follow.

class StorageManager: NSObject {

 let privateMOC: NSManagedObjectContext!
 
 private override init() {
   privateMOC = CoreDataManager.sharedManager.updateContext
 }

 private static var SMInstance: StorageManager?


 lazy var managedObjectContext: NSManagedObjectContext = {

      return CoreDataManager.sharedManager.persistentContainer.viewContext
 }()

}

My CoreData stack as follow.

class CoreDataManager {

 static let sharedManager = CoreDataManager()

 let persistentContainer: NSPersistentContainer!
 let viewContext: NSManagedObjectContext!
 let updateContext: NSManagedObjectContext!

private init() {

let container: NSPersistentContainer = {

let container = NSPersistentContainer(name: "Store")

  container.loadPersistentStores(completionHandler: { (_, error) in

    if let error = error as NSError? {
      fatalError("Unresolved error \(error), \(error.userInfo)")
    }
  })
    return container
    
}()

self.persistentContainer = container
self.viewContext = persistentContainer.viewContext

//This is where I use the privateQueueConcurrencyType formy privateMOC
let _updateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
_updateContext.parent = self.viewContext
self.updateContext = _updateContext

} 

Stacktrace as follow.

enter image description here

enter image description here

Upvotes: 2

Views: 431

Answers (2)

lorem ipsum
lorem ipsum

Reputation: 29464

I can't replicate your crash but I was getting an Error in the line you highlighted. I didn't have info on you SubProduct so I used the generic Item that comes with Xcode Projects

There are a ton of comments that come with the code.

This is just a standard View to test functionality.

struct CoreDataBackgroundView: View {
    @StateObject var vm: CoreDataBackgroundViewModel2 = CoreDataBackgroundViewModel2()
    
    public init(){}
    var body: some View {
        VStack{
            List(vm.items){ item in
                Button(action: {
                    vm.fetchItem(timestamp: item.timestamp)
                }, label: {
                    Text(item.timestamp!.description)
                })
            }
            Text("fetched Item = \(vm.item?.timestamp?.description ?? "nil")")
            Text(vm.items.count.description)
            Button("fetch", action: {
                vm.fetchItems()
            })
            Button("add", action: {
                vm.addItem()
            })
        }
    }
}

struct CoreDataBackgroundView_Previews: PreviewProvider {
    static var previews: some View {
        CoreDataBackgroundView()
    }
}

This is how I incorporated your code

class CoreDataBackgroundViewModel2: ObservableObject{
    private let manager = StorageManager.SMInstance
    @Published var items: [Item] = []
    @Published var item: Item? = nil
    ///Fetch everything
    func fetchItems() {
        do{
            items = try manager.fetchData(entity: "Item", model: Item.self)
        }catch{
            print(error)
            items = []
        }
    }
    ///This way you can just fetch the item(s) that you need
    func fetchItem(timestamp: Date?)  {
        if timestamp != nil{
            do{
                item = try manager.fetchData(entity: "Item", model: Item.self, NSPredicate(format: "timestamp == %@", timestamp! as CVarArg)).first
            }catch{
                print(error)
                item = nil
            }
        }else{
            item = nil
        }
    }
    
    func addItem() {
        manager.addItem()
    }
    
}
//Something had to change here there is no entry
class StorageManager: NSObject {
    let manager = CoreDataManager.sharedManager
    let privateMOC: NSManagedObjectContext
    
    private override init() {
        privateMOC = manager.container.newBackgroundContext()
    }
    //I made it a singleton
    static var SMInstance: StorageManager = StorageManager()
    
    lazy var managedObjectContext: NSManagedObjectContext = {
        return manager.container.viewContext
    }()
    func fetchData<T: NSFetchRequestResult>(entity: String, model: T.Type, _ custom_predicate: NSPredicate?=nil) throws -> [T] {
        let request = NSFetchRequest<T>(entityName: entity)
        
        if custom_predicate != nil {
            request.predicate =   custom_predicate
        }
        
        request.returnsObjectsAsFaults = false
        //Didn't get a crash but an Error
        //Value of optional type 'NSManagedObjectContext?' must be unwrapped to refer to member 'fetch' of wrapped base type 'NSManagedObjectContext'
        // It is bad practice to use ! try to minimize those as much as possible
        return try privateMOC.fetch(request)
        
    }
    func addItem()
    {
            privateMOC.perform {
                let newItem = Item(context: self.privateMOC)
                newItem.timestamp = Date()
                newItem.text = "sample"
                do{
                    try self.privateMOC.save()
                }catch{
                    print(error)
                }
                
            }
        
    }
    
}
//I changed your manager because there were a bunch of nil objects that are unnecessary also, I added the ability to use an inMemory conxtext for when you are using previews/canvas
//This is basically the standard setup with comes with all New Projects in Xcode
//Some of the errors probably had to do with the let variables that were after loadPersistentStores. They were called before the store was done loading so they stayed nil. The speed of the load might have given you mixed results.
class CoreDataManager {
    static let sharedManager = CoreDataManager.previewAware()
    private static func previewAware() -> CoreDataManager{
        if ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1"{
            return CoreDataManager.preview
        }else{
            return CoreDataManager.shared
        }
    }
    private static let shared = CoreDataManager()
    
    
    private static var preview: CoreDataManager = {
        let result = CoreDataManager(inMemory: true)
        let viewContext = result.container.viewContext
        for n in 0..<2 {
            let newItem = Item(context: viewContext)
            newItem.timestamp = Date()
        }
        do {
            try viewContext.save()
        } catch {
            // Replace this implementation with code to handle the error appropriately.
            // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            let nsError = error as NSError
            fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
        }
        return result
    }()
    
    let container: NSPersistentContainer
    
    init(inMemory: Bool = false) {
        //This is usually the AppName
        container = NSPersistentContainer(name: "AppName")
        //If you are in Preview the setup work take place only on a device or simulator
        if inMemory {
            container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
        }
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                
                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        
        container.viewContext.automaticallyMergesChangesFromParent = false
        container.viewContext.mergePolicy = NSMergePolicy.mergeByPropertyStoreTrump
    }
}

Upvotes: 0

Stamenkovski
Stamenkovski

Reputation: 1044

When you are using background context (private queue), you should wrap it in perform or performAndWait. With that said the fetch method should be called like this:

context.performAndWait { 
    context.fetch(request)
}

Because the queue is private and internal to the NSManagedObjectContext instance, it can only be accessed through the perform(:) and the performAndWait(:) methods.

More about using core data in background read here

Edit 1:

performAndWait takes a closure as its parameter that has no return value, so you can't return result/value from it. You need to understand these concepts in Swift.

Let's take your problem for example, you have a function that you want to return array of some values after the fetch request is performed. What you did is not going to work because of what I said earlier, so we will "extract" the context outside of the function.

func fetch<T>(_ type: T.Type, in context: NSManagedObjectContext) -> [T] { 
  return context.fetch(request)
}

In that way we can wrap the function in whatever context we want either background or viewContext(main).

context.performAndWait { 
   let products = fetch(SubProduct.self, in: context)
   // do some other stuff
}

Upvotes: 2

Related Questions