Create variable of type NSFetchRequestResult that can be used as NSFetchRequest result type

I am trying to teach myself core data. I have code that works including the function below, if I hard code the result type, that will test a persistent store to see if it is empty and if so, read in data from disk and store it to persistent stores. I would like to make this function generic so I can use it for all 3 entities/classes that I have created. The problem I am unable to solve is how to create a variable that I can insert into of NSFetchRequest. If there is a better way to solve the problem I am all ears. Thank you.

func addModelValuesToContainerIfEmpty(container: NSPersistentContainer, fundName: String) {  // will pass in one of three fund names
    
    let fileName: String = "Formatted" + fundName
    let entityName: String = fundName + "Entity"
    let resultType: NSFetchRequestResult = String(fundName + "Entity") as! NSFetchRequestResult    
    do {
        let request = NSFetchRequest<resultType>(entityName: entityName)    // error = cannot find type 'resultType' in scope
        let numRecords: Int = try container.viewContext.count(for: request)
        if numRecords == 0 {
            let data = GetDF(fileName: fileName)
            let lastRowIndex: Int = data.shape.rows - 1
            for i in 0...lastRowIndex {
                let newDailyCloseEntity = VOOEntity(context: container.viewContext)
                let tempDFRow: DataFrame.Row = data[row: i]
                newDailyCloseEntity.date = stringDateFormatter.date(from: tempDFRow[0] as! String)
                newDailyCloseEntity.close = tempDFRow[1] as! Float
                
                do {
                    try container.viewContext.save()
                } catch let error {
                    print("Error saving. \(error)")
                }
            } // end for loop
        }
    } catch {
        print("Error")
    }
}

I've had a similar problem a while ago and solved it like this:

    @objc protocol CommonAttributes: NSFetchRequestResult { // this protocol lists all attributes my different entities have in common, I used the autogenerated classes for them
        init(context: NSManagedObjectContext) // I needed this elsewhere
        var date: Date? { get set }
        // etc.
    }

    // the three entities that already have a date attribute can conform to this protocol like so:
    extension MyFirstEntity: CommonAttributes { }
    extension MySecondEntity: CommonAttributes { }
    extension MyThirdEntity: CommonAttributes { }

    // in whatever type performa the generic fetch:
    func genericFetch<T: CommonAttributes>(usingCreator requestCreator: () -> NSFetchRequest<T>, andContainer container: NSPersistentContainer) {
        let fetchRequest = requestCreator() as NSFetchRequest<T>
        do {
            let numRecords = try container.viewContext.count(for: request)
            if numRecords == 0 {
                // I just use a date in this example, so:
                let newEntry = T(context: container.viewContext)
                newEntry.date = Date()
                try container.viewContext.save()
            }
        } catch {
            print("Error")
        }

    // using the fetchMethod like this on, for example, MyFirstEntity:
    genericFetch(usingCreator: MyFirstEntity.fetchRequest, andContainer: myPersistentContainer) // note that I pass MyFirstEntity.fetchRequest as a closure, I don't invoke it here!

Note that I rely on the static methods provided by the autogenerated classes for the entities here, if you use specific types you might need to adapt. I originally wanted to include the fetchRequest method in my CommonAttributes protocol, but that didn't work (it's been a while, so I assume that was due to the "Self requirements" problems for existentials; perhaps that's now possible to do slightly better with the new stuff since Swift 5.6). Then I'd have used the method signature

    func genericFetch<T: CommonAttributes>(forType typeToFetch: T.Type, andContainer container: NSPersistentContainer)

    // call like:
    genericFetch(forType: MyFirstEntity.self, andContainer: myContainer)

If you want to avoid passing the fetchRequest() method as parameter and cannot include it in the protocol, you can also simply constrain T to your CommonAttributes and NSManagedObject, then do a soft cast to T:

    func genericFetch<T: CommonAttributes & NSManagedObject>(forType typeToFetch: T.Type, andContainer container: NSPersistentContainer) {
        let fetchR = T.fetchRequest() // this comes from NSManagedObject's definition and just gives an NSFetchRequest<NSFetchRequestResultType> object back
        // ...
    }

Since you just want to count the objects this might be easier, you can still use T(context:) to create new elements. I think this gives the correct count (99 % sure), but better double check that it doesn't give the summed count for all 3 entities... :sweat_smile:.
If you want to do anything concretely with fetched objects from this, you have to cast them to T like this:

    if let fetchedObjects = container.viewContext.fetch(fetchR) as? [T] {
        // manipulate the attributes defined in `CommonAttributes`
    }

Of course I am assuming that CommonAttributes includes all the attributes/properties you need to define a valid (non-constraint-violating) new object in all above, but that's a problem you have to address in any case for every possible generic code for this. You can work with several protocols for different things in the different entities and then do different things in different if let asThirdEntity = oneFetchedElementThatIsT as? SomeAttributes or even ... as? MyThirdEntity, I think you get the gist.

Thank you. Works great. Below is the function call and updated function:

genericDiskFetch(usingCreator: VOOEntity.fetchRequest, andContainer: container, withFundName: "FormattedVOO")

@objc protocol CommonAttributes: NSFetchRequestResult {
    init(context: NSManagedObjectContext) // I needed this elsewhere
    var date: Date? { get set }
    var close: Float { get set }
}
extension VOOEntity: CommonAttributes {}
extension VFIAXEntity: CommonAttributes {}
extension PrincipalEntity: CommonAttributes {}

func genericDiskFetch<T: CommonAttributes>(usingCreator requestCreator: () -> NSFetchRequest<T>, andContainer container: NSPersistentContainer, withFundName fundName: String) {
     let fetchRequest = requestCreator() as NSFetchRequest<T>
    do {
        let numRecords = try container.viewContext.count(for: fetchRequest)
        if numRecords == 0 {
            let data = GetDF(fileName: fundName)
            let lastRowIndex: Int = data.shape.rows - 1
            for i in 0...lastRowIndex {
                let newDailyCloseEntity = T(context: container.viewContext)
                let tempDFRow: DataFrame.Row = data[row: i]
                newDailyCloseEntity.date = stringDateFormatter.date(from: tempDFRow[0] as! String)
                newDailyCloseEntity.close = tempDFRow[1] as! Float
                do {
                    try container.viewContext.save()
                } catch let error {
                    print("Error saving. \(error)")
                }
            } // end for loop
        }  // end if
    } catch {
        print("Error")
    } // end catch
}
1 Like