Implement the flow to share data between iCloud users using Core Data CloudKit.
More and more people own multiple devices and use them to share digital assets or collaborate work. They expect seamless data synchronization across their devices and an easy way to share data with privacy and security in mind. Apps can support such use cases by moving user data to CloudKit and implementing a data sharing flow that includes features like share management and access control.
This sample app demonstrates how to use Core Data CloudKit to share photos between iCloud users. Users who share photos, called owners, can create a share, send out an invitation, manage the permissions, and stop the sharing. Users who accept the share, called participants, can view or edit the photos, or stop participating the share.
Before building the sample app, perform the following steps in Xcode:
To run the sample app on a device, configure the device as follows:
To create and configure a new project that uses Core Data CloudKit, see Setting Up Core Data with CloudKit.
CloudKit apps must have a schema to declare the data types they use. When apps create a record in the CloudKit development environment, CloudKit automatically creates the record type if it doesn’t exist. In the production environment, CloudKit doesn’t have that capability, nor does it allow removing an existing record type or field, so after finalizing the schema, be sure to deploy it to the production environment. Without doing that, apps that work in the production environment, like the App Store or TestFlight ones, would not work. For more information, see Deploying an iCloud Container’s Schema.
Core Data CloudKit apps can use initializeCloudKitSchema(options:) to create the CloudKit schema that matches their Core Data model, or keep it up to date every time their model changes. The method works by creating fake data for the record types and then delete it, which can take some time and blocks the other CloudKit operations. Apps must not call it in the production environment, or in the normal development process that doesn’t include model changes.
To create the CloudKit schema for this sample app, pick the “InitializeCloudKitSchema” target from Xcode’s target menu, and run it. Having a target dedicated on CloudKit schema creation separates the initializeCloudKitSchema(options:) call from the normal flow. After running the target, be sure to check with CloudKit Console if every Core Data entity and attribute has a CloudKit counterpart. See Reading CloudKit Records for Core Data for the detailed mapping rules.
For apps that use CloudKit public database, manually add a Queryable index for the recordName and modifiedAt fields of all record types, including the CDMR type that Core Data generates to manage many-to-many relationships.
For more information on this topic, see Creating a Core Data Model for CloudKit
To create and share a photo using the sample app, follow these steps:
To discover more features of the sample app:
It may take some time (minutes or longer) for one user to see the changes from the others. Core Data CloudKit is not for real-time synchronization. When users change the store on their device, it is up to the system to determine when to synchronize the change. There is no API for apps to speed up, slow down, or choose the timing for the synchronization.
Every CloudKit container has a private database and a shared database. To mirror these databases, set up a Core Data stack with two stores, and set the store’s database scope to .private and .shared respectively.
When setting up the store description, enable persistent history tracking and turn on remote change notifications by setting the NSPersistentHistoryTrackingKey and NSPersistentStoreRemoteChangeNotificationPostOptionKey options to true . Core Data relies on the persistent history to track the store changes, and apps need to update their UI when remote changes occur.
For apps (under the same developer team) to synchronize data through CloudKit, they must use the same CloudKit container. This sample app explicitly specifies the same container for its iOS and watchOS apps when setting up the CloudKit container options:
Sharing a Core Data object between iCloud users includes the following tasks:
NSPersistentCloudKitContainer provides methods for creating a share ( CKShare ) for Core Data objects and managing the interaction between the share and the associated objects. UICloudSharingController implements the share invitation and management. Apps can implement a sharing flow using these two APIs.
To create a share for Core Data objects, call share(_:to:completion:) . Apps can choose creating a new share, or adding the objects to an existing share. Core Data uses CloudKit zone sharing so each share has its own record zone on the CloudKit server. (For more details, see WWDC21 session 10015: Build Apps that Share Data Through CloudKit and Core Data and WWDC21 session 10086: What’s new in CloudKit.) CloudKit has a limit on how many record zones a database can have. To avoid hitting the limit, consider using an existing share if appropriate.
See the following method for how this sample app shares a photo:
NSPersistentCloudKitContainer doesn’t automatically handle the changes UICloudSharingController (or other CloudKit APIs) makes on a share. When the kind of changes happen, apps must update the Core Data store by calling persistUpdatedShare(_:in:completion:) . The sample app implements the following UICloudSharingControllerDelegate method to persist a updated share.
Similarly, when owners tap the “Stop Sharing” button or participants tap the “Remove Me” button in the CloudKit sharing UI, NSPersistentCloudKitContainer doesn’t immediately know the change. To avoid stale UI in this case, implement the following delegate method to purge the Core Data objects and CloudKit records associated with the share using purgeObjectsAndRecordsInZone(with:in:completion:) .
Core Data doesn’t support cross-share relationships. That is, it doesn’t allow relating objects associated with different shares. When sharing an object, Core Data moves the whole object graph (including the object and all its relationships) to the share’s record zone. When users stop a share, Core Data deletes the object graph. In the case where apps need to reserve the data when users stopping a share, make a deep copy of the object graph and make sure no object in the graph is associated with any share.
When importing data from CloudKit, NSPersistentCloudKitContainer records the changes on Core Data objects in the store’s persistent history, and triggers remote change notifications ( .NSPersistentStoreRemoteChange ) so apps can keep their state up to date if necessary. The sample app observes the notification and does the followings in the notification handler:
To process the persistent history more effectively, the app:
This is the code that sets up the history fetch request ( NSPersistentHistoryChangeRequest ):
For more information about persistent history processing, see Consuming Relevant Store Changes.
In the CloudKit environment, duplicate data is sometimes inevitable:
To remove duplicate data (or deduplicate), implement a way that allows all peers to eventually reserve the same winner and remove others. The sample app removes duplicate tags in the following way:
The sample app only detects and removes duplicate tags from the owner side because participants may not have write permission. That is, deduplication only applies to the private persistent store.
See the following method for the code that deduplicate tags:
When UICloudSharingController is unavailable or doesn’t fit the app UI, consider implementing a custom sharing flow if necessary. ( UICloudSharingController is unavailabe on watchOS. On macOS, use NSSharingService with the .cloudSharing service.) To do that, here are the steps and relevant APIs:
In the whole process, whenever changing a share using CloudKit APIs, call persistUpdatedShare(_:in:completion:) so Core Data persists the change to the store and synchronize it with CloudKit. As an example, this sample uses the following code to add a participant