Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 461
Joris416 Avatar asked Jul 13 '18 10:07

Joris416


1 Answers

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)
    }
}
like image 136
vadian Avatar answered Nov 02 '22 12:11

vadian