Introduction
Cloud synchronization is a regular trend of the last few years. If you are developing for one or several Apple platforms (iOS, macOS, tvOS, watchOS) and the task is to implement synchronization functionality between applications, then you have a very handy tool, or even a whole service - CloudKit.
Our team constantly has to screw up the functionality of data synchronization with CloudKit, including in projects that use CoreData as a data warehouse. Therefore, the idea arose, and then the idea was implemented - to write a universal interface for synchronization.
CloudKit is not just a framework. This is a full BaaS (Backend as a Service), i.e. comprehensive service with a full-fledged infrastructure, including cloud storage, push notifications, access policies and much more, as well as offering a universal cross-platform program interface (API).
CloudKit is easy to use and relatively affordable. Just for being a member of the
Apple Developer Program , at your disposal for free:
')
- 10Gb storage for resources
- 100MB for the database
- 2GB of traffic per day
- 40 requests per second
And these numbers can be increased if there is such a need. It is worth noting that CloudKit does not use the user's iCloud storage. The latter is used only for authentication.
This article is not an advertisement for CloudKit or even another review of the basics of working with it. There will be nothing about setting up a project, configuring the App ID in your developer profile, creating a CK container or Record Type in the CloudKit dashboard. In addition, not only the backend component remains outside the article, but also the entire software component directly related to the CloudKit API. If you would like to understand exactly the basics of working with CloudKit, then there are already excellent introductory articles for this, to repeat which makes no sense.
This article is in a sense the next step.
When you are already familiar with something that you have been using for a long time, sooner or later the question arises: how to automate the process, make it even more convenient and more unified? This is how design patterns came about. This is how our framework, which makes it easier to work with CloudKit - ZenCloudKit, which has already been successfully applied in a number of projects, arose. About him, namely, about the new technical way of working with CloudKit, and will be discussed further.
Universal interface
Creating the framework, our ultimate goal was to implement such an interface that would be compatible with CoreData entities, allowing us to synchronize — save, delete and retrieve data — with a minimum of effort, taking into account the existing database connections, regardless of the complexity of the existing architecture.
The framework is written in Swift 3 and it is Swift-developers who are fully able to experience the benefits of its use. For Objective-C, a fully-fledged bridge is possible, but for obvious reasons, similar things will look in it redundant and more cumbersome to implement. The code examples in this article will be written in Swift.
Let us turn to the review, in parallel considering the example of implementation.
Implementation example
Consider as an introduction some typical synchronization operations: save and delete methods. The final implementation is as follows:
What is going on here?
Suppose we have an
event object with an
entity property, where the
entity is an NSManagedObject. This NSManagedObject, like any database object, has fields, some of which are properties, some are links,
reference , to other NSManagedObject objects, forming one-to-one or one-to-many links.
To save this object (or delete the corresponding one) synchronously or asynchronously to the CloudKit database, while forwarding all the connections, the proxy object is used - iCloud, which contains the appropriate methods. It is enough to call entity.iCloud.save () (asynchronous) or entity.iCloud.saveAndWait () (synchronous saving) so that all fields of the entity are recorded in the corresponding fields of the CloudKit object, and the unique UUID from the newly saved CKRecord (i.e. string The recordName property of the CKRecordID object) was automatically written back to the specially designated field of the entity object, thereby forming a link between the local and remote object.
If you have never used CloudKit and it all sounds incomprehensible, then it is easier to say that any entity has .iCloud.save () and this is enough to keep both the object and all its connections. No more sets of identical methods for different entities and dirt in the client code. Convenient, isn't it?
Setting up sync objects
In order for this to work, you must fulfill several conditions.
The work is based on the widely used property mapping scheme, which is used in many libraries, in various web parsers (such as RestKit), etc. Mapping is implemented in the classical manner - by means of KVC, which is supported only by the heirs of NSObject. Hence, the first condition:
1) Each synchronized object must be an NSObject descendant (for example, NSManagedObject is an excellent choice).
2) Each synchronized object must implement the ZKEntity protocol, which looks like this:
If you work with CoreData, then you need to implement directly in your (sub-) class:
As can be seen from the protocol, the required fields are recordType and mappingDictionary. Consider both.
// REQUIRED (required fields)1)
recordType - the corresponding record type, Record Type, in CloudKit.
Example: the Person class contains the recordType = “Person” property. After the save () call on its instance, an entry will be made in the CloudKit dashboard in this table (“Person”).
Implementation:
static var recordType = "Person"
2)
mappingDictionary - property mapping dictionary.
Scheme: [local key: remote key (field in the CloudKit table)].
Example: the Person class contains the fields firstName and lastName. To save them to the Person table in CloudKit with the same names, you need to write the following:
static var mappingDictionary = [ "firstName" : “firstName”, “lastName” : “lastName” ]
// OPTIONAL (optional fields)The remaining protocol fields are optional,
3)
syncIdKey is the name of a local property that will store the ID of the remote object. ID - is the passport of the object, necessary for communication local <-> remote.
The field is conditionally optional. When initializing the controller framework, which will be described below, it is possible to specify the name of the property for all entities. However, specified individually in the class of the entity, it has a higher priority and it will be checked first at parsing. And only then, if the implementation is empty, the universal key will be used (see below).
Implementation:
static var syncId: String = "cloudID"
changeDateKey is the name of a local property that will hold the date the object was changed. Another utility property required for synchronization.
Similar to the previous one, it is conditionally optional. It is possible to omit the implementation and specify the name of the property for all synchronized objects during the initialization of ZenCloudKit (see below).
Implementation:
static var changeDateKey: String = "changeDate"
references is a dictionary containing keys that implement the * -c-one connection.
Scheme: [“local key”: “remote key”]
The requirement here is that the property “local key” with its type has a class that satisfies the basic requirements (inherits NSObject and implements the ZKEntity protocol).
When you call save () on a local object, ZenCloudKit will also try to save everything associated with it.
Implementation:
static var references : [String : String] = ["homeAddress" : "address"]
referenceLists is a dictionary containing an array of ZKRefList objects, each of which carries information about a specific * -to-many relationship: the type of objects and the name of the key for which you need to query and save this list.
Schema: ZKRefList (entityType: ZKEntity.Type,
localSource: a local property that returns an array of ZKEntity objects,
remoteKey: key in CloudKit for storing an array of links (CKReference))
Implementation:
static var referenceLists: [ZKRefList] = [ZKRefList(entityType: Course.self, localSource: "courseReferences", remoteKey: "courses")]
courseReferences is a user-defined property that returns an array of ZKEntity objects that you would like to keep and links to which should be placed in the list of links of the root object.
Code (continued):
var courseReferences : [Course]? { get { return self.courses?.allObjects as? [Course] } set { DispatchQueue.main.async { self.mutableSetValue(forKey: "courses").removeAllObjects() self.mutableSetValue(forKey: "courses").addObjects(from: newValue!) } }
Implementing the appropriate setter is also necessary so that the application can save objects derived from CloudKit. Thus, the localSource field of the ZKRefList object is essentially a reference to a handler (handler) that controls input and output operations.
isWeak is an optional flag that, when set (true), indicates that any other object that refers to an instance of this type forms a weak link (analogy with the weak modifier) ​​in CloudKit. This means that the record of it will be deleted in cascade, as soon as the object that contains the link to it is deleted.
Example: there is an object A that refers to object B.
If you set B.isWeak = true, object A will be saved to CloudKit with a “weak link” to B. Object B will be deleted automatically as soon as you delete object A.
This flag is an implementation of the CloudKit native API and appeals to the CKReference constructor with the .deleteSelf flag:
CKReference.init(record: <CKRecord>, action: .deleteSelf)
Therefore, the removal mechanics is entirely the prerogative of CloudKit, while the framework simply offers a more user-friendly interface. In the future, this functionality can be expanded so that cascade deletion can be configured for different entities.
Implementation:
static var isWeak = true
referencePlaceholder is a property that, when declared, avoids the nil value when an object is received from CloudKit, replacing it with a default value.
If it is assumed that the CoreData entity object should always contain some value other than nil as a reference to another object, then whenever this object is absent from CloudKit during synchronization, the local property can be automatically set to a default value.
Example: there is a class A with property b and, mirroring to it, the same Record Type in CloudKit.
CloudKit has an object A, which is absent locally and has an empty reference to B (the value is absent). In a typical scenario, as a result of synchronization, you would get an object A whose property b would be nil. But with the default value set in the local class (
referencePlaceholder = ...), ZenCloudKit will automatically assign the value you specify to property b:
Ab = referencePlaceholder,
where the latter is an instance of B.
So, as a result of a full synchronization cycle, objects with full links will always be created in your application, even if they were kept empty on all other devices.
Implementation:
static var referencePlaceholder: ZKEntity = B.defaultInstance()
Please note that the referencePlaceholder is specified in the target class. If it is necessary that property b of object A does not turn out to be nil (Ab! = Nil), then it is in class B that you must implement referencePlaceholder, and not in the root class A, which we received as a result of synchronization.
// SUMMARYAt the time of this writing, this is all the functionality supported by ZKEntity. Summarize the above again as a concrete example.
Suppose there is an Event class:
A ZKEntity implementation might look like this:
Here:
- dictionary for mapping properties.
- link mapping dictionary (optional)
- CloudKit Record Type
Omitted syncIdKey and changeDateKey. In the example, they correspond to the syncID and changeDate properties. Since similar properties (changeDate, syncID) are present in the interface of other classes, they were recorded in the initialization phase of ZenCloudKit (which will be discussed later) as universal, so the private implementation was omitted.
Configuring the controller and delegate
After the entities have been configured, you need to initialize the controller and assign its delegate. This can be done in various ways, but the best thing to do is to allocate a separate class for this and write the called initializer.
For starters, you can create a global variable that will store a link to a static controller instance.

The delegate class will need to implement the following protocol:
Before considering each method separately, let's look at the version of the finished implementation (with the exception of the zenSyncDIdFinish method).
The CloudKitPresenter class in the example is a ZenCloudKit delegate. Here is the initialization and call call-functions necessary to implement the full synchronization cycle. A full synchronization cycle is a sequence of offscreen operations in which local and remote objects are compared in terms of change time and updated at both ends. For this, for each type, i.e. for each registered ZKEntity entity, the framework must provide three functions that implement the creation, request for an object by ID (fetch) and request for all available objects, respectively. In each of the three functions, the ZKEntity class (ofType T: ZKEntity.Type) acts as a parameter. As a result of execution, ZenCloudKit expects to receive objects of this particular type.
zenAllEntities (ofType T: ZKEntity.Type)- expects to receive an array of all T-type entities
zenCreateEntity (ofType T: ZKEntity.Type)- expects to receive a new copy of T.
zenFetchEntity (ofType T: ZKEntity.Type, syncId: String)- expects to get an existing instance of T on the given syncId (or nil if there is none).
For example, if you work with the entities Person and Home, then the parameter T in these functions will be equal to one of these two types. Your task is to provide a result for each of them (a new object, an existing one and everything). This can be done either by performing type checking and writing code for each, or using interface polymorphism.
In the given example, for the implementation of the above operations, standard MagicalRecord methods are used to search for the existing one, create a new one and query all objects that work as extension-methods (or category methods, to put it in the spirit of Objective-C) for NSManagedObject. This greatly simplifies implementation. The code becomes universal, since there is no need to do a type-check for each T case.
Functions are a concrete implementation of generic abstraction, although, strictly speaking, generalizations in the function signature are not used for purposes of compatibility with Objective-C.
The last function uses the T.predicateForId (...) instruction. This is the extension method provided by ZenCloudKit, which returns the correct search predicate for a given type of T by the given syncId (to avoid a hard code and the associated possible errors in the name of the property that locally stores the ID).
zenEntityDidSaveToCloud (entity: ZKEntity, record: CKRecord ?, error: Error?)- Called every time you save to CloudKit. In this phase, the entity has already received the remote object ID, so here you can, for example, save the main database context.
The delegate implements a private singleton (sharedInstance is not visible to the client). In order to initialize both the controller and its delegate, it is enough to call the method somewhere from the outside at the right time:

In the initialization method, the framework is configured:
Standard parameters for CloudKit are set:
- container name
- database type (ofType: .public / .private)
This is followed by the syncIdKey and changeDateKey keys already discussed above — the names of the properties that store the ID of the entries and the date of the change. It should be noted that these values ​​can be left blank (nil). In this case, when calling the appropriate methods on ZKEntity instances (for example, save ()), ZenCloudKit will look for their implementation among the declarations of each class. Conversely, it is sufficient to specify these keys only here in order to omit the specific implementation. If both public and private implementation are empty, then calling cloudKit.setup () will generate an error in the log, and synchronization will not work.
In the entities parameter we pass an array of all the types we are going to work with.
ignoreKeys is an array of string keys that, upon detecting which, ZenCloudKit should ignore the object (for example, do not save or delete it).
deviceId - device ID. A very important parameter if several devices are involved in synchronization. The developer should take care of the uniqueness of this parameter. As standard, the Hardware UUID is taken, but other options are possible.
// RECAPThe implementation of the settings described so far is a necessary and sufficient condition for the basic functionality provided by the iCloud proxy to work, which, in turn, implements the ZKEntityFunctions protocol:
With the exception of the update () function, the purpose of which is to update a local object from a remote object, represented in the code as CKRecord. This function should be used in the delegate's zenSyncDIdFinish method, which is called at the end of the full synchronization cycle, which, in turn, is started as follows:
The first option is synchronization in standard mode. Each subsequent synchronization cycle is fixed by ZenCloudKit; if successful, the last sync date is saved (the framework takes care of all this). Saving the date is very important: it allows you to select only those objects whose date of change is later than the date of the last successful cycle. Otherwise, if, say, you have 100 objects in the database, then each cycle would include a meaningless check of long-synchronized, unchanging objects. It is completely unnecessary and, moreover, resource-intensive operation.
The second option is forced synchronization (forced: true). There may be times when data integrity is compromised. Then you can forcefully check each synchronized object, ignoring the date of the last successful cycle, and update the data locally and remotely. Local objects will be updated by what lies in CloudKit (if for some reason this has not happened before). And in CloudKit, local objects can be saved, which also for some reason have not been saved. Depending on the specifics of your application, you yourself can determine where to force synchronization (for example, when starting, during a long idle time, or to take this function to the settings). In general, there is no need for this call and, most likely, you will not have to resort to it.
Calling the syncEntities () method at the controller level does the same only for all registered entities. The specific parameter accepts specific types that you would like to synchronize (nil - if you want to apply to all).
It remains to analyze the zenSyncDIdFinish method, the signature of which looks like this:
Options:
T - the type of entity whose objects you want to create or update.
newRecords ,
updatedRecords - arrays of CKRecord, objects that need to be created or updated locally. The reference point when searching for a local match is a unique ID, which is stored as standard in the CKRecord.recordID.recordName property. The entity, among the objects of which you need to look for matches and an instance of which to create, is T.
DeletedRecords is an array of
ZKDeleteInfo objects, each of which stores information about the object to be deleted: the local ZKEntity type and object ID. These objects can be of different types, so it is not necessary to focus on type T in this case. The type of the object to be deleted should be viewed in the entityType property, and the object ID in the syncId property of the ZKDeleteInfo object. The class looks like this:
ZenCloudKit generates this list before completing the deletion by sending it to the zenSyncDidFinish handler in the deletedRecords array so that you can perform the necessary local cleanup. Once locally everything is successfully removed, you need to call the callback method finishSync (). If this is not done, then no changes will be made to the CloudKit database. Such a scheme is adopted for security reasons: only by making sure that the local database is updated, you call the finalizer - finishSync ().
Always call finishSync () at the end of synchronization.This applies not only to the deletion phase described above, but also to the creation and update phases.
To summarize, consider a fragment of the implementation of the zenSyncDIdFinish function:
Immediately after this fragment should follow:
- call finishSync ()
- UI update functions that would reflect the changed state of the database (if required).
With the following instructions:

we fill the fields of the local object with the fields CKRecord, which is available to us as an argument in one of the arrays. The fetchReferences flag allows you to load all links. By loading links, we mean the actual loading of the corresponding objects (listed in the
references and
referenceLists arrays described in the ZKEntity protocol) from CloudKit and their binding to this
entity . If, when loading a connection, it is found that the corresponding local object does not exist (
zenFetchEntity == nil ), it will be automatically created in the local database by calling the delegate's
zenCreateEntity method.
If the formation of these links implies a change in the UI, you need to take care of this additionally (updateEntity - in terms of filling links - works asynchronously and you should not wait for its execution). In the
ZKRefList handler
, this can be done in the setter, as already mentioned:
Here the following happens:
When receiving * -to-many connections (as a result of calling updateEntity with the flag fetchReferences = true), an array of Teacher objects gets into the setter teacherReferences. In the main thread, we update this list at the root NSManagedObject, and then call the UI update methods.
Mapping connections * -to-one (an array of references containing the name of the properties-links to other entities ZKEntity) does not imply handlers (get / set), so if you want to track the formation of these connections, you need to resort to a similar method - as keys in the references array to specify handlers and override their getter and setter — either use ReactiveCocoa or other means to monitor the properties.
Working with links seems rich in nuances, and this is true, but these nuances are a natural consequence of the binding and automation of the work of two systems - CoreData and CloudKit.
If you need to have more direct control over linking, updating UI, or other sync-related processes, you can combine the ZenCloudKit and the native CloudKit API at your own discretion. In the zenSyncDidFinish method, arrays of CKRecord objects are passed, which, in addition to properties, contain CKReference objects. This means that you can customize the parsing, as well as manually download the objects that you need.
This completes the ZenCloudKit setup.
Nuances of use
The standard way to access the framework functionality is through an instance (singleton) of the ZenCloudKit controller:
As arguments, all the same instances and classes of ZKEntity.
The abbreviated version (through the .iCloud proxy class) is currently available only in Swift.
Push notifications
Handling push notifications can also be transferred to ZenCloudKit:
The result of his work is a call to the zenSyncDIdFinish delegate method, with one of the three filled arrays (newRecords, updatedRecords, deletedRecords), which automatically leads to updating the database and the UI (if you took care of this in the body of this function). Let me remind you that the usual scenario of handling push notifications involves a number of fairly monotonous actions: checking the type of notification (CKNotification), the reason for the notification (queryNotificationReason), parsing - determining the entity to which the notification relates, and only then calling the appropriate handler. ZenCloudKit takes it all on itself.
Synchronization lock
Sooner or later, your application code will be filled with .save () or .delete () instructions in different places. ( ), - , :

, , false. .
. / debugMode ( true):
:
ZenCloudKit Record Type, query- modifiedDate (CKRecord). . , Device DeleteQueue. , . — — - , ( — ). , , DeleteQueue . , .
Security
ZenCloudKit .
CloudKit : (1) , — (2) . , 15 ( ), . CloudKit : (fetch), nil, ( ). , , CloudKit. (. GCD), , CloudKit API , , QoS CKQueryOperation.
ZenCloudKit, ZKEntity, . 15 — 3 ( 5 ), , “ ” 5 , - . (DoS).
Conclusion
. , . — CloudKit, CoreData. .
(, , ). : , CKAssets ( ).
- . ZenCloudKit - , .