Reputation: 5577
Let's consider this example use case, an iOS app for recipes:
Public data (shared by all users, read only, download on demand): When the user firstly opens the app he finds a shallow list (not fully downloaded) 10 recipes (fetched from remote server) with the option of downloading the full recipe and then he can opens the details screen of the recipe. And at any point the list can grow and the user should always have the latest data (fetch from remote and keep in sync). This data should be available offline and should pull new content when online. (Read only)
Private data (specific for a user): The user can create a custom recipe which is stored locally and synced remotely. This data should be available offline and should synced when online (Read and write)
Data should be synced in all iOS devices
What I'm considering is using Core Data (offline) and CloudKit (remote). But I'm not sure if this can handle the scenario described above or if it has any limitations. Do you think that Core Data (offline) and CloudKit is the best option ? any limitations ? what other options do you recommend
Upvotes: 1
Views: 2975
Reputation: 1589
The architecture you have discribed in your question is one that works well using Core Data and CloudKit. There may be other options (such as Firebase, Realm) that may be worth exploring, but since you've mentioned Apple frameworks, I'll limit my answer to those. This is all from my own experience, so your mileage may vary.
Let's start with the simplified answer.
Apple offers the NSPersistentCloudKitContainer
which encapsulates a Core Data stack and mirrors the persistent store to a CloudKit private database. Although this is a great solution for new apps, it can not be used with existing CloudKit containers. "Core Data owns the CloudKit schemea created from the Core Data model. Existing CloudKit containers aren't compatible with this schema." But it still may be a path worth exploring and there are plenty of resources available:
Now with that out of the way, let's dig into some specifics.
Before the NSPersistentCloudKitContainer
, apps were required to mange syncing and storage between the Core Data and CloudKit frameworks. Although this may seem like a daughting task, it is a managable one. There are a few things to have in mind as you approach a solution:
It&'s best to treat your CloudKit stores as the source of truth.
Maintaining a unique identifier between a Core Data record and a CloudKit record is essential.
Primarily your app/user experience should be driven from the Core Data store.
Treating tasks - like creating recipes, persisting recipes, updating recipes - as Operation
s may be benificial.
The public/private/shared access is core to how CloudKit functions. A CloudKit container has three databases (CKDatabase
):
privateCloudDatabase: Owner readable, owner writable. Not visable to you through the Developer Portal.
publicCloudDatabase: World readable, owner writable. Can be locked down by Roles, and is visable to you through the Developer Portal.
sharedCloudDatabase: Available to the share participants (CKShare
), and not visible to you.
This structure fits well with your desire to have a separation between public and private/user data.
Subscriptions (CKSubscription
) can be setup on the databases to notify you that changes have occured. At which point you can have your app fetch the records with changes. You can supply a change token (CKServerChangeToken
) as a way to limit your query results to only the records that have changed since the last download. Each device has its own subscription & token so it will download only the data and record changes it needs. In this way, CloudKit is the source of truth, and all changes - reguardless of where they occur - are reflected on other devices.
You do not have to wait for a notification to occur in order to query for changes. CKFetchRecordZoneChangesOperation
will always provide a new change token at the competion of execution. So retrieval of changes is the same process no mater how it is triggerd; via notification or some other mechanism like connectivity changes, or manually.
You would want to create a subscription for both the 'public' and 'private' databases to ensure that changes could be monitored for both.
There is an important difference in how your model will be structured between Core Data and CloudKit, and this comes in the form of relationships.
Core Data recommends that all relationships are bi-directional. For instance: A Recipe
would have a relationship to its Ingredient
s (one to many), AND an Ingredient
would have a relationship to its Recipe
(one to one).
In CloudKit, only uni-directional relationships are recommended, using a CKReference
. So for the example given, a Recipe
would not have a direct reference to the ingredients, but the Ingredient
would have a relationship reference to the Recipe
which uses it.
As part of interoperating between Core Data and CloudKit, you'll want a identifier that is unique and queriable between both environments. A common way to achieve this is to use a UUID
. The CKRecord.ID
is composed of a 'recordName' (String), and a 'zoneID' (CKRecordZone.ID
). Supplying the UUID.uuidString
as the 'recordName' will create unique records in CloudKit, but also give you a consistent reference to entities in Core Data.
As you've indicated, offline access to data is critical to the usability of your app. Core Data is a great option here. Even though you have the to display results directly from a CKQuery
, persisting that data does have some advantages. I treat the local Core Data store as the interface to all data in my app. This means there is a single interface to my users data.
Any change that is made to the local data, is also sent to CloudKit, and any record that comes from CloudKit is added or modified in the local Core Data store.
Using Core Data in this fashion allows you to take advantage of tools like the NSFetchedResultsController
which can be used to automatically reflect changes made to a set of entities.
Also, you can hook into the Notification
NSManagedObjectContextDidSave
do help keep your apps UI up-to-date when changes are merged from CloudKit interactions.
Remember, trying to perform extensive tasks using the NSPersistentContainer.viewContext
can lead to performance issues, so you'll want to familiarize yourself with DispatchQueue
s.
CloudKit interactions are operation-based. Queries & writes are defined as subclasses of Operation
. Each task is defined and then added to a queue to be processed. These operations are performed asynchronously. I find it usefull to extend this style of programming out to other areas of your app.
For instance: when I create a new Recipe
, that is an Operation
against Core Data, and then I have an Operation
to create that record in CloudKit. The second operation relies on the first, and is canceled if the first doesn't succed.
It becomes a little easier to think about the coordination between Core Data and CloudKit as a series of tasks that need to be executed. A change notification is delivered to your app > Fetch changes (using change token) > Add/Update those records in Core Data > Update UI (automatically/manually). Each is a link in the chain and is dependant on the last to be completed before moving forward.
You can use foundations Operation
and OperationQueue
, or a framework like ProcedureKit to help with this process.
Overall, Core Data and CloudKit integrate well together, and are commonly used for the type of scenario you are trying to achieve. Depending on the complexity or state of development you are at, NSPersistentCloudKitContainer may be a good starting point. But once, you step beyond its features, you'll want to be aware of the challenges and features pointed out above.
Upvotes: 5