Slightly easier Core Data manipulation

:  ~ 1 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.