Update 2023-06
As of iOS 17 et al, *all* of the CloudKit DiscoverUser APIs have been deprecated, with no alternatives provided. So now it is NOT possible to create the kinds of social experiences that Apple has in their first-party apps.
Introduction
CloudKit, the technology behind iCloud, enables iOS apps to synchronize data across a user’s devices, as well as share data between users, with solid privacy and security features built-in. Apple has been adding data sharing into many of its first-party apps, including:
- Files
- Reminders
- Notes
- Calendar
- Photos
- Activities
- Find My
- Maps
- Music
- Health
In iOS 15, Apple has made data sharing easier for third-party apps, thru better documentation of CKShare in CloudKit as well as additional features in NSPersistentCloudKitContainer in CoreData.
In this article, I will review the data sharing aspects of CloudKit, and then visually examine how Apple’s first-party apps implement sharing from a user-experience (UX) perspective. Finally, I will discuss how Apple's generic UIs and privileged access to privacy permissions makes it much harder for third-party apps to implement sharing in a similarly way.
CloudKit Fundamentals
As the name implies, CloudKit is Apple’s cloud-based storage technology, designed specifically to synchronize a user’s data across all of their devices.
Fundamentally, it is organized into container, user, database, and record objects. Each app that wants to use CloudKit gets its own container, and each user of that app gets assigned a user record with a unique ID. There is one public database in the container, which contains user records, and each user gets their own private database.
Each container is unique and separate, and the user IDs in each container are random and unique. A user ID in one app won’t be the same in any other app. In this way, neither Apple nor any third-party developers can harvest info about a user, what apps they use, or what records they create.
CloudKit Sharing
For sharing in CloudKit, a share object allows a user to expose a private record with an optional set of participants. A record can be shared publicly, where anyone can access it, or shared privately, where only the set of participants can access it. These share objects and their shared records become visible to the participants thru a shared database, which is really just a controlled view into someone else’s private database.
The tricky bit of sharing is configuring a share object with participants. As users have random and unique IDs in each container, how does one user discover the identity of another user? When an app wants to implement sharing in CloudKit, it must ask each user to become “discoverable”. This is called the “Look Me Up” permission, and the permission dialog looks like this:
(Have you not seen this permission dialog before?! Well, that’s because it seems Apple’s first-party apps get privileged access to Look Me Up. More on this later.)
Once a user grants this permission, the app can provide an email address to CloudKit and receive the unique ID of that user, which can then be used to add a participant to a share. Note that this is a one-way lookup: given an email address the app may be able to get a user, but given a user, the app does not get an email address.
Additionally, CloudKit can return a list of “friends" for the current user. It does this by using privileged access to the user’s Contacts, and returning a list of unique IDs for contacts that also use the app. Again note that this is a one-way lookup, as the app itself does not gain access to the user’s Contacts directly.
Once users are identified and a record in a private database is shared, how do the participants receive the shared record? The share object for that record has a URL which must be transmitted to each participant. When the app receives the URL, it queries CloudKit for the share object, and can then either accept or ignore the share. When accepted, CloudKit updates the state of the participant in the share object accordingly, and the record being shared becomes visible to that user via the shared database. Note that the URL is like a "secure envelope" that only invited participants can fully “open". Thus the URL can be distributed unsecured without compromising the privacy of the owner or the participants.
CloudKit Sharing UI
CloudKit provides some system UI components to make sharing easier for developers to implement. Let’s look at the Files app to see how it uses these system UI components to share files and folders.
First, navigate to a file or folder, long press for quick actions, and choose Share. Tap the “Share in iCloud” action.
The iCloud Sharing UI (aka UICloudSharingController) is presented. When starting a new share, it identifies what is being shared and the user's Apple ID. The user can set the access and permissions options, and send an “invitation".
Tapping Messages brings up a Messages thread UI, with the URL of the share already added to a new message. The user can start typing a name in the To field, and matching contacts will appear below. Tapping a contact adds it, and tapping send will send the message. The UI for Email and Copy Link are similar.
Note that contacts turn blue when they have an Apple ID (and also have the app?). Behind the scenes, CloudKit has retrieved the addresses entered and added those users as invited participants to the share object.
The message gets delivered as normal. On the receiver's side, when the user taps the link, iOS retrieves information about the share and presents its details and options.
If the user chooses “open", the app is launched with the share information, which then calls CloudKit to accept the share. At this point, both the owner and the participants share the folder and all of its contents, and any of them can choose the “Manage Shared” action to update permissions or end sharing.
Participants can remove themselves, and see who else is sharing.
The owner can add or remove participants, change permissions, and stop sharing altogether.
Removing a participant or stopping a share will prevent participants from accessing the URL and its share record again.
UICloudSharingController provides all the basics for initiating and managing data sharing. Both Files and Reminders, as well as Apple’s productivity apps Keynote, Numbers, and Pages, use UICloudSharingController.
Apple’s first-party apps customize UICloudSharingController to each experience. Notes is the most extensive with UI to support change tracking, but these levels of customization are not at all exposed to third-parties.
Third-parties can only provide an icon and title for the record being shared. Note however that the heading is generic and the description is confusing.
I don’t know about you, but even I didn’t know that CloudKit records are really iCloud Drive files underneath the hood.
Custom CloudKit Sharing UX
As is, while UICloudSharingController provides a basic sharing experience, it requires users to explicitly send and receive URLs with each other. It conflates sharing links with sharing data, and requires users to switch between several apps to achieve data sharing.
Of course, developers can implement CloudKit sharing directly, and many of Apple’s first-party apps skip UICloudSharingController completely. Let’s visually examine several of them to see how.
Photos - Shared Albums
Let’s explore how Photos allows users to share albums.
First, the user creates the shared album by naming it and adding participants, and then posts photos to it.
Participants are notified by the Photos app. The user can accept or decline directly within the Photos app.
When they respond, the owner is also notified. The owner can manage a variety of options for the shared album.
Find My - Friends
Now let’s explore how friends find each other IRL.
As always, a user starts the share by inviting users. Note here that Find My has already identified some friends even before the user has typed anything.
On the recipient’s side, Find My automatically accepts the share without the participant taking any action, and then asks the user to reciprocate in sharing.
This can lead to imbalanced sharing between friends, and users can manage this accordingly.
Asking to follow becomes an inverted invitation to share.
Maps - ETA
Finally, let’s see how users share their ETAs.
After a user starts a route, they can tap Share ETA.
Notice again that Maps has already identified potential recipients. Tapping on one adds them to the shared route.
On the recipients’ side, Maps automatically accepts the share and starts notifying the user of progress.
The user can track their friend, stop receiving notifications, or block the sender from sharing ETA in the future.
Sharing in Third-Party Apps
So, can third-party developers create data sharing experiences like Apple? Absolutely*
*Except for two permissions that must be explicitly requested: "Look Me Up” and “Access Contacts”. I’ll return to this below.
As you can see from Apple’s implementations, CloudKit enables numerous ways to design and implement data sharing experiences. Apps must focus their UX and UI on how share URLs are easily configured, delivered, and received.
Let’s explore these three aspects. I’ll start with delivery, and finish up with configuration.
Delivery
There are two ways to think about delivering share URLs: Indirect or Direct.
UICloudSharingController implements indirect delivery. The sender must choose the means and configure the message. The receiver must manually receive the message and specifically engage the share URL. For many data sharing situations, this can work just fine.
But in other situations, indirect delivery introduce too much friction and ambiguity, and so direct delivery must be implemented. I don’t know *how* Apple implements direct delivery for their first-party apps, but I suspect it involves public “InboxInfo” records which contain a user ID and share URL pointing to publicly shared private “Inbox” records. Senders can find the InboxInfo of a recipient, accept the share URL of the Inbox record, and then append "Invite” and/or “Request” records to the Inbox.
Implementing something like this requires deep consideration for privacy and security. I would love to see Apple supply direct delivery as part of CloudKit.
Responses
There are fundamentally three potential responses to a share URL: ask, accept, and reciprocate. As you’ve seen above, each has their place in different kinds of sharing interactions.
I’ve written my own “Friendship” sharing engine on CloudKit that requires both users to reciprocate sharing of a “Profile” record. If and when either user removes a friend’s profile (aka unfriending), the reciprocated sharing participants are each removed. I’ll be writing more about this friendship engine separately.
Configuration
Configuring the participants of a share object is, IMHO, the trickiest bit. All of Apple’s first-party apps have privileged access to both "Look Me Up” and “Access Contacts” permissions, which allows their user experiences to flow more naturally and without interruptions. In particular, all of their apps have this UI below to add recipients:
This one screen is (likely) searching the user's contacts and then searching CloudKit for a matching user, but doesn’t trigger either the “Look Me Up” or “Access Contacts” permissions dialogs. Since the "Look Me Up” permission doesn’t include Contact info, third-party apps must request both permissions in order to implement a screen like this.
And that’s a shame, for several important reasons. First, it’s challenging enough to fit a single permissions dialog into a user experience (let alone two in a row). Second, and more significantly still, users are trained by Apple apps to NOT expect these permission dialogs at all, so its jarring and unfamiliar when they do appear. And third, and perhaps most inhibiting, users won’t be able to participate in custom sharing experiences until they first launch the app and enable “Look Me Up”.
I think Apple could address this in one of two ways.
First, Apple could add these permission dialogs to their own first-party app experiences. This would be inline with all the other permissions that Apple has introduced to their platforms and their own apps, like Location and Camera.
Or, Apple could formalize the UI screen above, and introduce a CKPickParticipantsController to return fully-discovered participants. Similar to CNPickContactsController in the Contacts framework, this would work without triggering the “Access Contacts” permission dialog, since the user would be granting permission implicitly by identifying another user explicitly, and would work without users needing to enable “Look Me Up” first.
Conclusion
In this article, I introduced the fundamental building blocks for data sharing in CloudKit. Then we visually examined the various ways Apple uses CloudKit to enable sharing in their own apps. And finally, I outlined how third-party developers can implement their own data sharing via CloudKit, and ways Apple can make this easier and more consistent, while remaining private and secure.
I’ll write a separate article about my “Friends” implementation of CloudKit Sharing. I also recommend Dan Griffin’s article "CloudKit Sharing: Five Tips and Tricks”. Much of my work was inspired by Costantino Pistagna’s article “Custom CloudKit sharing workflow”.