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:
class ProductModel: NSManagedObject { }
let request = ProductModel.fetchRequest() // <- Ambiguous use of fetchRequest().
let request1: NSFetchRequest<ProductModel> = ProductModel.fetchRequest() // <- Works properly.
Update, Oct 10, 2017: It has been solved, all of this is now redundant. Yay!
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.