Streaming is available in most browsers,
and in the Developer app.
-
Get the most out of CloudKit Sharing
Discover how apps can use CloudKit to share records with others. We'll show you how to encourage collaboration between people using your app and support those interactions with Apple frameworks. Learn how to create and manage shares, explore sharing options like public permissions, and find out how you can use zone sharing in iOS 15 and macOS Monterey to share entire record zones of data. To get the most out of this session, we recommend being familiar with CloudKit and a basic understanding of record and data types.
Resources
Related Videos
WWDC23
WWDC21
-
Download
Simon Manning: Hi, I'm Simon, and I'm an engineer on the CloudKit team.
Today I'm going to demonstrate how to use CloudKit to support sharing data between users of an app.
I'll start by introducing our Sharing sample app, available on GitHub.
Then I'll walk through how to support creating shares and inviting other users to participate in those shares in an app.
I'll discuss how your app can share and accept invitations on behalf of participating users, how to fetch shared records, how your app can provide a customized sharing experience, and lastly, the zone-based sharing model available in CloudKit.
First up, the Sharing sample app.
As a reminder, CloudKit is a framework that gives your application access to a database on iCloud.
This is exposed in the API as a CKContainer through which you can access multiple CKDatabases.
Each container has one public database where all users can potentially read and write records.
If the device has a logged-in iCloud account, your app also has access to a private database which contains that user's data.
And if your app supports sharing, then data which is shared with the current iCloud user will be available to your app in their shared CKDatabase.
To demonstrate both initiating shares from the record-owning user and accepting invitations to a share as a participant, I'll be referring to a sample app available on the Apple GitHub account.
Sharing is a minimal example of a contacts app.
It supports creating new contact records in the user's private database, sharing individual contacts with other users of the app, and displaying all contacts shared with the current user from others.
The Sharing repository contains an implementation using Swift concurrency, as well as a completion handler-based implementation, available on separate branches.
To begin sharing, first create a new contact record and save it to the owner's private database.
The addContact function is called by Sharing's Add New Contact UI.
It creates a new CKRecord with a recordType of Contact and stores a name and phone number.
The record is then saved to the private database.
Sharing a contact record with other users of the Sharing app begins by creating a share.
Sharing in CloudKit relies on a specialized kind of record called a CKShare.
When your user wants to share records with others, create a CKShare on their behalf and save it to the private database.
Because this user owns all the records being shared, they're also the owner of the share record itself.
This share record is the source of truth for what data is being shared, who it's being shared with, and what permissions are available to share participants.
Share permissions are how the owner of the share can control access to the shared records.
The publicPermission property represents the permission level for any user with a link to the share.
By default, users with just the URL have no access.
Setting this property to a more permissive value allows any user with the share's URL to join.
Each invited participant also has a permission level determined by the share's owner.
Participant objects describe a specific user's participation in a share, including the user's identity and their invitation acceptance status.
Permissions are set on each participant object individually and each participant is added to the participants array on the CKShare.
To begin sharing records from a user's private database, first determine the root record of the share.
The root record and all its child records will be included in this share.
Then, create the CKShare record and save both the root record and CKShare to the current user's private database.
In Sharing, the createShare function initializes a CKShare with the contact record to be shared.
The CKShare and root record are saved to the owner's private database, and the CKShare is returned back to the UI to start the invitation process.
Both the CKShare and root record must be saved together in the same operation.
Saving only the CKShare will generate an error, even if the root record already exists in CloudKit.
Sharing a root record also shares its child records.
Now that your app is creating shares, I'll cover how you can configure and send user invitations to participate in a share.
The sample app uses a view controller provided by UIKit named UICloudSharingController.
Implementing this controller in your app allows users to easily invite participants to their CKShare, set up permissions, view and manage participation, or stop sharing records all together.
Here's how UICloudSharingController appears in the Sharing sample app when sharing a new contact record.
Users can choose how to send share invitations, and permissions can be configured in the Share Options section.
Implement the controller's delegate protocol to provide additional information about the share, as well as receive notifications about sharing events and errors.
In addition to the methods described here, there are methods to configure the item title, item type, and thumbnail image to display on share invitations.
Now that the owner of the share has sent invitations to others, your app needs to process and accept the share on behalf of the invited participants.
Enable the CKSharingSupported boolean in your app's Info.plist.
This lets the system know that your app supports sharing, and should process invitation URLs and call the relevant delegate functions.
When a share invitation is opened and accepted, your app will receive a CKShare.Metadata object through delegate method callbacks.
Your app uses this metadata to inform CloudKit that the current user is accepting and participating in this share through the accept function on the container, or through a CKAcceptSharesOperation.
With an applicationDelegate lifecycle, the system will launch your app and call the userDidAcceptCloudKitShareWith: shareMetadata delegate method.
Initialize a CKContainer object using the identifier provided in the shareMetadata object.
Then call accept on the CKContainer, passing in the shareMetadata object, to let CloudKit know the current user has accepted the share.
Now that your app is sending invitations to share private records and processing and accepting those invitations, I'll cover how your app can fetch and modify shared records that users are participating in.
Records shared with the current iCloud user are available inside the app container's sharedCloudDatabase.
The same CloudKit APIs for fetching, querying, and manipulating data inside private and public databases can be used with the shared database.
In the sample app, shared records are fetched in the fetchSharedContacts function.
It uses the recordZoneChanges operation to progressively fetch and process changes.
Each time this operation completes, it returns a moreComing boolean property, representing whether there are more changes to fetch.
It also returns a changeToken, which is a pointer to a specific change in the zone's history.
You can use this changeToken to only fetch changes that occur after that point in time.
For the first fetch, or to refetch all changes in a zone's history, set the changeToken to nil.
Managing active shares depends on the current user.
For owners of a share, the share can be stopped by deleting the CKShare record from the owner's private database.
The owner can also delete the shared root record entirely.
The removeParticipant function on the CKShare record object provides a way to remove individual participants, which can be found on the participants property.
And lastly, the UICloudSharingController UI makes it possible for owners to stop sharing with all participants.
For the participant user of a share, participation can be stopped by deleting the root CKRecord from the user's shared database, or using the UICloudSharingController UI.
Note that deleting the root CKRecord from the user's shared database only stops participation in the share -- the original root record would still exist in the owner's private database.
While the Sharing sample app uses UICloudSharingController to handle participant lookup and invitation, you can also implement this functionality with the CloudKit API and build a custom user experience for both owners and participants of a share.
First, to search for participants to invite to a CKShare, use the CKFetchShare ParticipantsOperation or shareParticipants functions on the relevant CKContainer.
Phone number, email, and user record IDs are supported as parameters.
Next, set the specific permission level for this participation, and add the participant to the CKShare with the addParticipant function.
Always save the modified CKShare back to the private database.
The saved CKShare record has a url property which can be sent to invited participants to accept and process.
An invitation URL can be used to fetch the share's CKShare.Metadata and confirm participation through the accept(shareMetadata) function on the app's container.
The Sharing app demonstrates CloudKit sharing with a hierarchical sharing model, where a single record is shared as a root record, and any children of that record are also shared.
Zone sharing, a new model introduced in iOS 15, allows entire zones of records to be shared instead.
When sharing a zone, all of the records in the zone are shared, not just a single record's hierarchy.
Because zone sharing affects all of the records in a record zone, this type of sharing cannot coexist with hierarchical shares in the same record zone.
You can either have one or more hierarchical shares in a zone, or a single zone-wide share.
Zone sharing and hierarchical sharing each have their benefits, and both should be utilized depending on your use case.
In the context of the Sharing sample app, zone sharing could be useful if groups of contacts were organized in separate record zones.
A zone share could be used to share those groups of contacts with other users.
To learn more about zone sharing, check out "What's new in CloudKit" from WWDC21.
To create a new zone-wide share, initialize it with the custom record zone's ID to be shared and save it to the private database.
The process for accepting share invitations and fetching records from the shared database is the same as before.
To determine if a share is a zone-wide share, check the recordName property of its recordID.
If the value is CKRecordNameZoneWideShare, the share is managing a shared record zone; otherwise, it's managing a shared record hierarchy.
Now that you know how to support sharing in your app, start creating CKShare records and allow users to collaborate with others.
Get a jump start on your sharing UI using UICloudSharingController, or build a custom implementation to fit your needs.
Use familiar fetch and modify API to access shared records from the shared cloud database.
And start exploring new sharing use cases in your app that could benefit from implementing zone sharing.
Thanks for watching!
-
-
1:58 - Create a new Contact record
// Create a new Contact record func addContact(name: String, phoneNumber: String) async throws { let id = CKRecord.ID(zoneID: recordZone.zoneID) let contactRecord = CKRecord(recordType: "Contact", recordID: id) contactRecord["name"] = name contactRecord["phoneNumber"] = phoneNumber try await privateCloudDatabase.save(contactRecord) }
-
4:09 - Preparing a CKShare
// Preparing a CKShare func createShare(contactRecord: CKRecord) async throws -> CKShare { let share = CKShare(rootRecord: contactRecord) try await privateCloudDatabase.modifyRecords( saving: [contactRecord, share], deleting: [] ) return share }
-
5:25 - UICloudSharingControllerDelegate
// UICloudSharingControllerDelegate public protocol UICloudSharingControllerDelegate { // ... // Called after the CloudKit sharing controller failed to save the share record. func cloudSharingController(UICloudSharingController, failedToSaveShareWithError: Error) // Called after the CloudKit sharing controller saves the share record. func cloudSharingControllerDidSaveShare(UICloudSharingController) // Called after the user decided to stop sharing the record. func cloudSharingControllerDidStopSharing(UICloudSharingController) }
-
6:27 - Processing an accepted share invitation
// Processing a user’s acceptance of a share invitation func application( _ application: UIApplication, userDidAcceptCloudKitShareWith shareMetadata: CKShare.Metadata ) { let container = CKContainer(identifier: shareMetadata.containerIdentifier) Task { do { try await container.accept(shareMetadata) } catch { // Handle errors that may occur } } }
-
7:24 - Fetching shared records
// Fetching records shared with the current iCloud user func fetchSharedContacts(in zone: CKRecordZone) async throws { var changeToken: CKServerChangeToken? = nil var moreChangesComing = true while moreChangesComing { let changes = try await sharedCloudDatabase.recordZoneChanges( inZoneWith: zone.zoneID, since: changeToken ) // Process changes as needed (modifications and deletions) processChanges(changes) moreChangesComing = changes.moreComing changeToken = changes.changeToken } }
-
9:16 - Search: Phone Number
// Search by phone number let phoneNumber = "417-555-9311" let participant = try await container.shareParticipant(forPhoneNumber: phoneNumber)
-
9:16 - Search: Email Address
// Search by email address let emailAddress = "dave_knox@icloud.com" let participant = try await container.shareParticipant(forEmailAddress: emailAddress)
-
9:16 - Search: Record ID
// Search by user record ID let participant = try await container.shareParticipant(forUserRecordID: recordID)
-
9:32 - Add participant to a share
// Add participant to existing CKShare record func addParticipant(_ participant: CKShare.Participant, to share: CKShare) async throws { participant.permission = .readWrite share.addParticipant(participant) try await privateCloudDatabase.save(share) }
-
9:47 - Confirm invitation acceptance
// Fetch CKShare.Metadata and confirm accepting share from a given URL func confirmShareParticipation(from url: URL) async throws { let shareMetadata = try await container.shareMetadata(for: url) try await container.accept(shareMetadata) }
-
11:14 - Share an entire record zone
// Create a CKShare sharing an entire record zone func createAndSaveShare(for zone: CKRecordZone) async throws -> CKShare { let share = CKShare(recordZoneID: zone.zoneID) try await privateCloudDatabase.save(share) if share.recordID.recordName == CKRecordNameZoneWideShare { // This is managing a shared record zone } return share }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.