iOSGeek
iOSGeek

Reputation: 5577

Storage options for offline and online iOS app with synchronization

Let's consider this example use case, an iOS app for recipes:

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

Answers (1)

richardpiazza
richardpiazza

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 Operations may be benificial.

 CloudKit

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.

CloudKit & Core Data Schemas

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 Ingredients (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.

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 DispatchQueues.

Operations

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

Related Questions