Creating generic Repository class for Core Data with swift - swift

I'm trying to create a generic single repository class that can both save and fetch whatever entity you ask it to, likely inferred by object type. I'm using this question as a base.
I was trying to bypass having to pass an NSManagedObjectContext in to an entity, and have the Repository do this instead. Is this possible?
The ideal method signature to save an entity would be the following, where entity is a subclass of NSManagedObject with a few properties:
Repository(persistentContainer).save(entity)
NSManagedObject Extension
extension NSManagedObject
{
class func createInContext<T>(context:NSManagedObjectContext, type : T.Type) -> T {
return unsafeDowncast(NSEntityDescription.insertNewObject(forEntityName: entityName(), into: context), to: self) as! T
}
class func entityName() -> String {
let classString = NSStringFromClass(self)
return classString.components(separatedBy: ".").last ?? classString
}
}
Repository Save Method
public func save<T>(entity: T) -> Void where T: NSManagedObject
{
self.container.performBackgroundTask { (context) in
let object = T.self.createInContext(context: context, type: T.self)
// Error checking removed for brevity
try! context.save()
}
}
The error that I get at runtime is:
Failed to call designated initializer on NSManagedObject class 'UUID'
and below it...
[AppName.Uuid setUuid:]: unrecognized selector sent to instance
Which one is it then? Is it possible to bypass having to pass the context in every time with this method or not? Is there an alternative solution to achieve what I am after?

Related

Populating objects from cloud records (or another external source) using a generic function

I'm building a generic API for my Swift applications. I use CoreData for local storage and CloudKit for cloud synchronization.
in order to be able to work with my data objects in generic functions I have organized them as follows (brief summary):
Objects that go in the CoreData Database are NSManagedObject instances that conform to a protocol called ManagedObjectProtocol, which enables conversion to DataObject instances
NSManagedObjects that need to be cloud synced conform to a protocol called CloudObject which allows populating objects from records and vice-versa
Objects I use in the graphic layer of my apps are NSObject classes that conform to the DataObject protocol which allows for conversion to NSManagedObject instances
an object of a specific class. What I would like this code to look like is this:
for record in records {
let context = self.persistentContainer.newBackgroundContext()
//classForEntityName is a function in a custom extension that returns an NSManagedObject for the entityName provided.
//I assume here that recordType == entityName
if let managed = self.persistentContainer.classForEntityName(record!.recordType) {
if let cloud = managed as? CloudObject {
cloud.populateManagedObject(from: record!, in: context)
}
}
}
However, this gives me several errors:
Protocol 'CloudObject' can only be used as a generic constraint because it has Self or associated type requirements
Member 'populateManagedObject' cannot be used on value of protocol type 'CloudObject'; use a generic constraint instead
The CloudObject protocol looks as follows:
protocol CloudObject {
associatedtype CloudManagedObject: NSManagedObject, ManagedObjectProtocol
var recordID: CKRecordID? { get }
var recordType: String { get }
func populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext) -> Promise<CloudManagedObject>
func populateCKRecord() -> CKRecord
}
Somehow I need to find a way that allows me to get the specific class conforming to CloudObject based on the recordType I receive. How would I Best go about this?
Any help would be much appreciated!
As the data formats of CoreData and CloudKit are not related you need a way to efficiently identify CoreData objects from a CloudKit record and vice versa.
My suggestion is to use the same name for CloudKit record type and CoreData entity and to use a custom record name (string) with format <Entity>.<identifer>. Entity is the record type / class name and identifier is a CoreData attribute with unique values. For example if there are two entities named Person and Event the record name is "Person.JohnDoe" or "Event.E71F87E3-E381-409E-9732-7E670D2DC11C". If there are CoreData relationships add more dot separated components to identify those
For convenience you could use a helper enum Entity to create the appropriate entity from a record
enum Entity : String {
case person = "Person"
case event = "Event"
init?(record : CKRecord) {
let components = record.recordID.recordName.components(separatedBy: ".")
self.init(rawValue: components.first!)
}
}
and an extension of CKRecord to create a record for specific record type from a Entity (in my example CloudManager is a singleton to manage the CloudKit stuff e.g. the zones)
extension CKRecord {
convenience init(entity : Entity) {
self.init(recordType: entity.rawValue, zoneID: CloudManager.shared.zoneID)
}
convenience init(entity : Entity, recordID : CKRecordID) {
self.init(recordType: entity.rawValue, recordID: recordID)
}
}
When you receive Cloud records extract the entity and the unique identifier. Then try to fetch the corresponding CoreData object. If the object exists update it, if not create a new one. On the other hand create a new record from a CoreData object with the unique record name. Your CloudObject protocol widely fits this pattern, the associated type is not needed (by the way deleting it gets rid of the error) but add a requirement recordName
var recordName : String { get set }
and an extension to get the recordID from the record name
extension CloudObject where Self : NSManagedObject {
var recordID : CKRecordID {
return CKRecordID(recordName: self.recordName, zoneID: CloudManager.shared.zoneID)
}
}
Swift is not Java, Swift is like C++, associatedType is a way of writing a generic protocol, and generics in Swift means C++ template.
In Java, ArrayList<String> is the same type as ArrayList<Integer>!!
In Swift (and C++) , Array<String> is NOT the same type as Array<Int>
So, you can't take an array of Arrays for example, you MUST make it an array of Array<SpecificType>
What did Apple do to make you able to make a "type-erased" array for example?
They made Array<T> extend Array<Any>.
If you want to immitate this in your code, how?
protocol CloudObject {
// Omitted the associatedtype (like you already done as in the replies)
//associatedtype CloudManagedObject: NSManagedObject, ManagedObjectProtocol
var recordID: CKRecordID? { get }
var recordType: String { get }
func populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext) -> Promise<NSManagedObject & ManagedObjectProtocol>
func populateCKRecord() -> CKRecord
}
Then make the "generic protocol", this would be useful in safely and performance programming when the resolving of protocol is known at compile time
protocol CloudObjectGeneric: CloudObject {
// Generify it
associatedtype CloudManagedObject: NSManagedObject, ManagedObjectProtocol
// You don't need to redefine those, those are not changed in generic form
//var recordID: CKRecordID? { get }
//var recordType: String { get }
//func populateCKRecord() -> CKRecord
// You need a new function, which is the generic one
func populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext) -> Promise<CloudObject>
}
Then make the generic protocol conform to the non-generic one, not to need writing 2 populateManagedObject functions in each implementation
extension CloudObjectGeneric {
// Function like this if the generic was a parameter, would be
// straightforward, just pass it with a cast to indicate you
// are NOT CALLING THE SAME FUNCTION, you are calling it from
// the generic one, but here the generic is in the return, so
// you will need a cast in the result.
func populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext) -> Promise<CloudObject> {
let generic = populateManagedObject(from record: CKRecord, in context: NSManagedObjectContext)
return generic as! Promise<CloudObject> // In Promises I think this
// will NOT work, and you need .map({$0 as! CloudObject})
}
}

Abstracting Core Data Fetch Request

func fetchRequestFromViewContext(nameOfEntity: NSManagedObject) {
let fetchRequest = NSFetchRequest<nameOfEntity>(entityName: "\(nameOfEntity)")
do {
let result = try? CoreDataStack.instance.viewContext.fetch(fetchRequest)
}
}
Trying to abstract the core data fetch request therefore making an argument of type managed object and passing it into the fetch request generic but is not letting me, am I on the right track on abstracting this core data fetch request?
NSFetchRequest(entityName:) takes a String but nameofEntity was given as an NSManagedObject. Change that to a String and then also pass in the type of the entity. You can use generics (the <T> below) to allow for any class conforming to NSManagedObject.
func fetchRequestFromViewContext<T: NSManagedObject>(nameOfEntity: String, type: T.Type) {
let fetchRequest = NSFetchRequest<T>(entityName: nameOfEntity)
do {
let result = try? CoreDataStack.instance.viewContext.fetch(fetchRequest)
}
}
To call this you would simply do:
fetchRequestFromViewContext(nameOfEntity: "YourEntity", type: YourEntity.self)

Conforming to a static function that returns "Self"

When trying to conform to NSItemProviderReading, I get the following error:
The protocol definition for this method is as follows:
public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> Self
The protocol static function returns type "Self", I've tried to change it to the name of the actual class, but then it won't conform to NSItemProviderReading anymore.
How does one return "Self" ?
Update:
This is what happens when I ask Xcode to fix it:
It appends as! Self, but then shows 2 errors and this warning, it looks confusing cause it seems that it wants to revert back to how it was before, returning the instance of the class in this case NameData
Self in a protocol is a requirement that conformance of the protocol use their own type. So you need to change Self to NameData in the return type of the method when you conform this in your class extension.
extension NameData: NSItemProviderReading {
static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> NameData {
return NameData(name: "Test")
}
}
And one more thing, you need to make your NameData class final.

How to call a static method on a class Template method?

I try to call a class method on a generic T: BaseModel where T can be a subclass of BaseModel.
For example Car.
In the case T should be Car, I want my class method to be called on the Car class.
However, It always ends up calling the BaseModel class method instead.
class func parse<T: BaseModel>(json: JSON, context: NSManagedObjectContext) throws -> T? {
return T.classParseMethod(json: json) //This never calls the Car.classParseMethod()
}
where
let carObject = parse(json:json, context:context) as? Car
Any help?
The casting is done after the function call so the generic constraint resolves to T = BaseModel. You want the function to know of the type so it can properly resolve the generic constraint:
func parse<T: BaseModel>(_ str: String) -> T? {
print(T.Type.self) // should print like: Car.Type
return T.parse(str) as? T
}
// Make the desired type known to swift
let car: Car? = parse("My Car Format String")
One solution that seems to work is:
to add
func myFunc<T: BaseModel>(_ type: T.Type,..) -> T? {
type.aClassFunc()
{
If I call the following, it works.
if let obj = myFunc(Car.self, ...) {
// obj will be of type Car
}
It seems really too much just to achieve this but there might be an underlying reason for it.

How to mock realm-cocoa in swift

I'm using realm-cocoa for my persistence layer. There is one of the classes depending on realm
class RealmMetaData : AbstractMetaData {
var realm: RealmInterface
var isFirstLaunch: Bool = false
init(realm: RealmInterface = try! Realm()) {
self.realm = realm
let results = realm.objects(MyClass.self)
self.isFirstLaunch = (results.count == 0)
if (self.isFirstLaunch) {
realm.write {
realm.add(MyClass())
}
}
}
// some code
}
protocol RealmInterface {
// using a protocol based approach of mocking
func objects<T: Object>(type: T.Type) -> Results<T>
func write(#noescape block: (() throws -> Void)) throws
func add(object: Object)
}
extension Realm: RealmInterface {
func add(object: Object) { self.add(object, update: false) }
// there is a method for Realm with signature: add(object:Object, update:Bool = false)
// but swift extension dose not permit default function parameter, hence the wrapping
}
Then in my test code, I can write a mocked version of RealmInterface and inject it to the RealmMetaData instance using Constructor Injection.
When implementing the mocked RealmInterface, I found that's very difficult to mock the objects function to return an empty list. Because the return type of the function signature Results<T> is a type provided by the Realm Framework and there is no empty constructor available. Here is where I'm stuck.
That Result<T> is a class with final keyword so I also can't subclass it to use it's private methods to fetch an empty collection.
Thanks in advance!
As I suggested in a comments you can just use an internal in-memory Realm inside your test class and forward all methods that return Result<T> to it.
I end up returning my own protocol instead of results. So I have implementation of this protocol with AnyRealmCollection<T> and the other with just [T] so I easily mock it in tests without any in-memory Realm object.