I'm writing a generic wrapper class for core data.
Here are some of my basic types. Nothing special.
typealias CoreDataSuccessLoad = (_: NSManagedObject) -> Void
typealias CoreDataFailureLoad = (_: CoreDataResponseError?) -> Void
typealias ID = String
enum CoreDataResult<Value> {
case success(Value)
case failure(Error)
}
enum CoreDataResponseError : Error {
typealias Minute = Int
typealias Key = String
case idDoesNotExist
case keyDoesNotExist(key: Key)
case fetch(entityName: String)
}
I've abstracted my coredata writes in a protocol. I'd appreciate if you let me know of your comments about the abstraction I'm trying to pull off. Yet in the extension I run into the following error:
Cannot convert value of type 'NSFetchRequest' to expected argument type 'NSFetchRequest<_>'
Not sure exactly how I can fix it. I've tried variations of changing my code but didn't find success...
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject
var persistentContainer : NSPersistentContainer {get}
var idName : String {get}
func loadFromDB(storableClass : ManagedObject.Type, id: ID) throws -> CoreDataResult<ManagedObject>
func update(storableClass : ManagedObject.Type, id: ID, fields: [String : Any]) throws
func fetch(request: NSFetchRequest<ManagedObject>, from context: NSManagedObjectContext)
init(persistentContainer : NSPersistentContainer)
}
extension CoreDataWriteManagerProtocol {
private func loadFromDB(storableClass : ManagedObject.Type, id: ID) -> CoreDataResult<ManagedObject>{
let predicate = NSPredicate(format: "%@ == %@", idName, id)
let fetchRequest : NSFetchRequest = storableClass.fetchRequest()
fetchRequest.predicate = predicate
// ERROR at below line!
return fetch(request: fetchRequest, from: persistentContainer.viewContext)
}
func fetch<ManagedObject: NSManagedObject>(request: NSFetchRequest<ManagedObject>, from context: NSManagedObjectContext) -> CoreDataResult<ManagedObject>{
guard let results = try? context.fetch(request) else {
return .failure(CoreDataResponseError.fetch(entityName: request.entityName ?? "Empty Entity Name")) // @TODO not sure if entityName gets passed or not.
}
if let result = results.first {
return .success(result)
}else{
return .failure(CoreDataResponseError.idDoesNotExist)
}
}
}
Additionally if I change the line:
let fetchRequest : NSFetchRequest = storableClass.fetchRequest()
to:
let fetchRequest : NSFetchRequest<storableClass> = storableClass.fetchRequest()
I get the following error:
Use of undeclared type 'storableClass'`
My intuition tells me that the compiler can't map 'parameters that are types' ie it doesn't understand that storableClass
is actually a type. Instead it can only map generics parameters or actual types. Hence this doesn't work.
EDIT:
I used static approach Vadian and wrote this:
private func create(_ entityName: String, json : [String : Any]) throws -> ManagedObject {
guard let entityDescription = NSEntityDescription.entity(forEntityName: entityName, in: Self.persistentContainer.viewContext) else {
print("entityName: \(entityName) doesn't exist!")
throw CoreDataError.entityNotDeclared(name: entityName)
}
let _ = entityDescription.relationships(forDestination: NSEntityDescription.entity(forEntityName: "CountryEntity", in: Self.persistentContainer.viewContext)!)
let relationshipsByName = entityDescription.relationshipsByName
let propertiesByName = entityDescription.propertiesByName
guard let managedObj = NSEntityDescription.insertNewObject(forEntityName: entityName, into: Self.persistentContainer.viewContext) as? ManagedObject else {
throw CoreDataError.entityNotDeclared(name: entityName)
}
for (propertyName,_) in propertiesByName {
if let value = json[propertyName] {
managedObj.setValue(value, forKey: propertyName)
}
}
// set all the relationships
guard !relationshipsByName.isEmpty else {
return managedObj
}
for (relationshipName, _ ) in relationshipsByName {
if let object = json[relationshipName], let objectDict = object as? [String : Any] {
let entity = try create(relationshipName, json: objectDict)
managedObj.setValue(entity, forKey: relationshipName)
}
}
return managedObj
}
But the following piece of it is not generic as in I'm casting it with as? ManagedObject
. Basically it's not Swifty as Vadian puts it:
guard let managedObj = NSEntityDescription.insertNewObject(forEntityName: entityName, into: Self.persistentContainer.viewContext) as? ManagedObject else {
throw CoreDataError.entityNotDeclared(name: entityName)
}
Is there any way around that?
My suggestion is a bit different. It uses static methods
Call loadFromDB
and fetch
on the NSManagedObject
subclass. The benefit is that always the associated type is returned without any further type cast.
Another change is throw
ing errors. As the Core Data API relies widely on throw
ing errors my suggestion is to drop CoreDataResult<Value>
. All errors are passed through. On success the object is returned, on failure an error is thrown.
I left out the id
related code and the update
method. You can add a static func predicate(for id : ID)
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject = Self
static var persistentContainer : NSPersistentContainer { get }
static var entityName : String { get }
static func loadFromDB(predicate: NSPredicate?) throws -> ManagedObject
static func fetch(request: NSFetchRequest<ManagedObject>) throws -> ManagedObject
static func insertNewObject() -> ManagedObject
}
extension CoreDataWriteManagerProtocol where Self : NSManagedObject {
static var persistentContainer : NSPersistentContainer {
return (UIApplication.delegate as! AppDelegate).persistentContainer
}
static var entityName : String {
return String(describing:self)
}
static func loadFromDB(predicate: NSPredicate?) throws -> ManagedObject {
let request = NSFetchRequest<ManagedObject>(entityName: entityName)
request.predicate = predicate
return try fetch(request: request)
}
static func fetch(request: NSFetchRequest<ManagedObject>) throws -> ManagedObject {
guard let results = try? persistentContainer.viewContext.fetch(request) else {
throw CoreDataResponseError.fetch(entityName: entityName)
}
if let result = results.first {
return result
} else {
throw CoreDataResponseError.idDoesNotExist
}
}
static func insertNewObject() -> ManagedObject {
return NSEntityDescription.insertNewObject(forEntityName: entityName, into: persistentContainer.viewContext) as! ManagedObject
}
}
The issue is that NSManagedObject.fetchRequest()
has a return type of NSFetchRequest<NSFetchRequestResult>
, which is non-generic. You need to update the definition of your fetch
function to account for this. Btw the function signatures of the default implementations in the protocol extension didn't actually match the function signatures in the protocol definition, so those also need to be updated.
You also need to change the implementation of fetch(request:,from:)
, since NSManagedObjectContext.fetch()
returns a value of type [Any]
, so you need to cast that to [ManagedObject]
to match the type signature of your own fetch
method.
protocol CoreDataWriteManagerProtocol {
associatedtype ManagedObject : NSManagedObject
var persistentContainer : NSPersistentContainer {get}
var idName : String {get}
func loadFromDB(storableClass : ManagedObject.Type, id: ID) throws -> CoreDataResult<ManagedObject>
func update(storableClass : ManagedObject.Type, id: ID, fields: [String : Any]) throws
func fetch(request: NSFetchRequest<NSFetchRequestResult>, from: NSManagedObjectContext) -> CoreDataResult<ManagedObject>
init(persistentContainer : NSPersistentContainer)
}
extension CoreDataWriteManagerProtocol {
private func loadFromDB(storableClass : ManagedObject.Type, id: ID) -> CoreDataResult<ManagedObject>{
let predicate = NSPredicate(format: "%@ == %@", idName, id)
let fetchRequest = storableClass.fetchRequest()
fetchRequest.predicate = predicate
return fetch(request: fetchRequest, from: persistentContainer.viewContext)
}
func fetch(request: NSFetchRequest<NSFetchRequestResult>, from context: NSManagedObjectContext) -> CoreDataResult<ManagedObject> {
guard let results = (try? context.fetch(request)) as? [ManagedObject] else {
return .failure(CoreDataResponseError.fetch(entityName: request.entityName ?? "Empty Entity Name")) // @TODO not sure if entityName gets passed or not.
}
if let result = results.first {
return .success(result)
}else{
return .failure(CoreDataResponseError.idDoesNotExist)
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With