Slightly easier Core Data manipulation

:  ~ 3 min read

Working with Core Data is getting easier and easier, but there are a couple of improvements I'd like to talk about, and I'd like to start with the auto-generated, generic NSFetchRequest. It's a step in the right direction, but the problem is that trying to use it without explicitly declaring its type won't work:

let request = ProductModel.fetchRequest() // <- Ambiguous use of fetchRequest().
let request1: NSFetchRequest<ProductModel> = ProductModel.fetchRequest() // <- Works properly.

I do hope it's just a matter of time until it's solved, but in the meantime, I thought it can be improved a bit, by making use of protocols:

protocol CoreModel {

    associatedType Entity: NSManagedObject

}

extension CoreModel {

    private static var name: String {
        return String(describing: Entity.self)
    }

    static var request: NSFetchedRequest<Entity> {
        return NSFetchRequest<Entity>(entityName: name)
    }
}

We have a computed name, and we use that to compute a generic NSFetchRequest, of the required type. We can now make use of this like so:

class ProductModel: CoreModel {

    typealias Entity = ProductModel

}

[...]

let request = ProductModel.request // This is an NSFetchRequest<ProductModel>
let results = context.fetch(request) // This would properly return [PersonModel]

There's one more improvement I'd like to mention, and it's related to the insertion of a model. When we fetch some data over the network we should first verify if it exists locally, and update that instead of trying to create a new one directly:

protocol CoreModel {

    static func create(in context: NSManagedObjectContext = theMainContext, id: String) -> Entity {
        let request = self.request
        request.fetchLimit = 1
        request.predicate = NSPredicate(format: "id == %@", id)

        if let result = (try? context.fetch(request))?.first {
            return result
        }

        let entity = NSEntityDescription.insertNewObject(forEntityName: name,
                                                         into: context) as! Entity
        entity.setValue(id, forKey: "id")
        return entity
    }

}

[...]

let product = ProductModel.create(id: "1")
// product.id = "1" -> This can be omitted.
product.title = "Bike"

[...]

// Save the context.

Here, we make use of the aforementioned generic request, set its limit to 1, since an id should be unique, and we check if it returns a result. If it does, we return it, otherwise we insert a new one, set the value of its id field to the passed in value.

Note: it's usually not a good practice to handle Core Data via setValue(_:forKey:), but use the model's properties instead; here, though, we can be quite sure that we won't misspell id, and we can also save a line of code when calling create.

Let me know @rolandleth if there's anything that can be improved, I'd love to chat about it.