Video #343: CloudKit Sync: Sharing
Episode: Video #343 Date: Oct 27, 2025 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep343-cloudkit-sync-sharing

Description
We add iCloud sharing and collaboration to our reminders app rewrite, so that multiple users can edit the same reminders list. It takes surprisingly little code, no changes to our feature’s logic, and handles all manner of conflict resolution and more.
Video
Cloudflare Stream video ID: 4ace547dafcf01b8c79da974fc8bbc34 Local file: video_343_cloudkit-sync-sharing.mp4 *(download with --video 343)*
References
- Discussions
- Sharing data with other iCloud users
- Apple’s documentation for sharing
- Creating CKShare records
- CKShare
- SQLiteData
- StructuredQueries
- 0343-sync-pt4
- Brandon Williams
- Stephen Celis
- Mastodon
- GitHub
- CC BY-NC-SA 4.0
- source code
- MIT License
Transcript
— 0:05
OK, we have shown an incredible amount of power in our SQLiteData library. Not only does it give us seamless synchronization of our SQLite database to CloudKit, and not only does it automatically handle conflict resolution without you having to think about it, and not only does it handle large binary assets in the background, but it is also easy to turn off and on so that you can be very precise about when user data is synchronized. Brandon
— 0:30
But can you believe it gets better? What if we said that with just a few more lines of code we can make it possible for our users to share a record with another iCloud user so that you two can collaborate? And further, all associations will automatically be shared. And we even have full support for permissions so that you can grant read-only access or read-write access.
— 0:56
And on top of all of that, we make all the underlying CloudKit metadata 100% publicly available so that you are free to grab that information, read from it, and do whatever you want with it. It is even accessible to you at the SQL querying level, which means you can write complex SQL queries that interact with CloudKit information.
— 1:17
It’s incredible to see, and we’re going to show it all off, but first let’s get the basics of sharing into place. Sharing with iCloud users
— 1:26
To explore the sharing capabilities of SQLiteData we are going to make it possible for the users of our reminders app to share an entire reminders list with another person. That will allow two or more people to collaborate on a single reminders list, including all of the reminders contained in the list.
— 1:43
To see how this can be done, we are going to head over to the SQLiteData docs to see what it has to say about iCloud sharing. There is a dedicated article called Sharing data with other iCloud users .
— 2:09
And right at the top there is an important note that let’s you know a special property needs to be added to your info list: Important To enable sharing of records be sure to add a CKSharingSupported key to your Info.plist with a value of true. This is subtly documented in Apple’s documentation for sharing .
— 2:30
So, let’s go to the info plist file, and add this key with its value set to true…
— 2:43
Next in the article we see the section Creating CKShare records :
— 2:53
The CKShare object from CloudKit is the mechanism by which one can share a record with another iCloud user. The SyncEngine exposes a method called share(record:configure:) for creating this object, and then you can use the UICloudSharingController from UIKit to present a nice UI to the user for sharing their record. This UI will allow the user to either send the invite directly to the user via text message, email, or any other kind of communication mechanism, as well as generating a public link that can be shared more broadly. The UI also makes it possible to control the permissions of the share. And it’s worth noting that this UICloudSharingController API provides a very refined, yet constrained, UI for sharing a record, and it is possible to create your own custom sharing UI from scratch, but we won’t get into those details right now.
— 3:54
So, let’s start by adding a button to our app that allows sharing a reminders list. There are a few places we could add this button, but we’ve already got swipe actions on the rows that display reminders lists, so let’s add another action to that: .swipeActions { … Button { } label: { Image(systemName: "square.and.arrow.up.fill") } .tint(Color.blue) }
— 4:34
Since we have an observable model we will just invoke a method on that to defer all logic for sharing to it: Button { model.shareButtonTapped(remindersList: row.remindersList) } label: { Image(systemName: "square.and.arrow.up.fill") }
— 5:10
And then we will implement this method: func shareButtonTapped(remindersList: RemindersList) { }
— 5:23
Next we will invoke the share method mentioned in the docs, but to get access to that we need to have a handle on the sync engine, which we can get via the @Dependency property wrapper: @ObservationIgnored @Dependency(\.defaultSyncEngine) var syncEngine
— 5:52
And now we can invoke the share method and pass along the record we want to share: syncEngine.share( record: remindersList, configure: <#(CKShare) -> Void#> )
— 6:16
This method is async and throwing, so we will make the surrounding method async, and we will catch any errors emitted: func shareButtonTapped(remindersList: RemindersList) async { do { try await syncEngine.share( record: remindersList, configure: <#(CKShare) -> Void#> ) } catch { } }
— 6:33
And since this method is now async we need to update the view to spin up an unstructured task when the share button is tapped: Button { Task { await model.shareButtonTapped(remindersList: row.remindersList) } } label: { Image(systemName: "square.and.arrow.up.fill") }
— 6:47
Some of the errors thrown by the share(record:) method are programmer error, such as if you share a record that is not shareable, which we will get into a moment. And other errors are not programmer error, and may be something you do want to notify the user of, such as if CloudKit is having intermittent problems.
— 7:09
We can distinguish between these errors by catching and handling CKError s, which is an error that CloudKit’s APIs will throw, and then just reporting all other issues, which are just programmer error: do { try await syncEngine.share(record: remindersList) { share in } } catch is CKError { // TODO: let user know CloudKit is having problems and to try again } catch { // NB: Programmer error reportIssue(error) }
— 7:56
The share(record:) method also takes a trailing closure that is handed one of those CKShare objects and we can use that as a way of customizing the share object: try await syncEngine.share(record: remindersList) { share in }
— 8:26
There are two main customizations you can make to the share. You can give it a title, and you can give it a thumbnail image. This allows you to customize how the shared record is presented to the other iCloud user when they are asked to accept the sharing invitation.
— 8:46
We can read about this in the docs for CKShare .
— 8:53
We will see that we can subscript into the CKShare to override its title and thumbnail image using the SystemFieldKey . It’s also possible to set the shareType , which corresponds to a uniform type identifier, or UTI, but we don’t need that.
— 9:11
So, let’s set the title to include the name of the reminders list being shared: try await syncEngine.share(record: remindersList) { share in share[CKShare.SystemFieldKey.title] = """ Join '\(remindersList.title)'! """ } And to get access to CKShare we do need to import CloudKit: import CloudKit
— 9:36
And heck, we do have a cover image for the reminders list. It would be pretty cool if we included that image into the share so that it was even more personal.
— 9:57
But, remember the image data is not stored directly in the remindersList value, and that’s a good thing because the vast majority of the time we do not need that image data. It’s only in special circumstances that we need that data, which is the situation we are in right now.
— 10:21
So, let’s perform a quick database query to fetch the cover image for the reminders list we are trying to share: let coverImage = try database.read { db in try RemindersListAsset .find(remindersList.id) .select(\.coverImage) .fetchOne(db) }
— 11:06
And we don’t expect this to error in any way that is the user’s fault. If it does throw an error, it is only programmer error, and so we will surround the database query in withErrorReporting : let coverImage = withErrorReporting { try database.read { db in try RemindersListAsset .find(remindersList.id) .select(\.coverImage) .fetchOne(db) } }
— 11:37
And now we have the image data to put into the thumbnailImageData system field: try await syncEngine.share(record: remindersList) { share in share[CKShare.SystemFieldKey.title] = """ Join '\(remindersList.title)'! """ share[CKShare.SystemFieldKey.thumbnailImageData] = coverImage }
— 11:43
OK, we are now invoking the share method successfully, but we aren’t doing anything with the value it returns. The value returned is known as a SharedRecord , which is a type in our SQLiteData library, and it’s a mostly opaque representation of the share. It holds onto the generated CKShare , as well as the underlying CloudKit container but that is a private detail, and this type is particularly useful for driving navigation to present the UICloudSharingController . It is even hashable and identifiable, which helps with navigation.
— 12:23
So what we need to do is hold onto a bit of extra state in our observable model: var sharedRecord: SharedRecord?
— 12:33
When this state because non- nil we will present the cloud sharing view, which will allow the user to determine who to share the record with.
— 12:40
We can now assign the output of the share method to this state: sharedRecord = try await syncEngine.share( record: remindersList ) { share in … }
— 12:45
And we can drive the presentation of a sheet from this state: .sheet(item: $model.sharedRecord) { sharedRecord in CloudSharingView(sharedRecord: sharedRecord) } This CloudSharingView is an API provided by SQLiteData, and it is just a SwiftUI friendly wrapper around UICloudSharingController because Apple does not provide one.
— 13:28
And it may be hard to believe, but this is all it takes to implement the feature in our app that lets our users share reminders lists with other iCloud users. There is still more work to be done for another user to be able to accept that share, but we will get to that in a minute.
— 13:49
For now let’s just test the code we have written so far. I am going to run the app on my phone, I’ll create a new reminders list called “Point-Free”, I’ll add a cover image, and I’ll go ahead and add a reminder, “Send weekly emails”, and tag it with “#easy-win”. A moment later we will see all of this new data has sync’d to iCloud: SQLiteData (shared.db) stateUpdate SQLiteData (private.db) nextRecordZoneChangeBatch: scheduled ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ event ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ → Sending │ reminderTags │ 2b267d92-4712-4ad8-b2d7-972cb479b910:reminderTags │ │ → Sending │ reminders │ 5c1a349d-5078-49c6-bfdd-4ef2d56a1c79:reminders │ │ → Sending │ remindersListAssets │ 25ddf013-186f-4c90-b72c-d5e9869afbbf:remindersListAssets │ │ → Sending │ remindersLists │ 25ddf013-186f-4c90-b72c-d5e9869afbbf:remindersLists │ └───────────┴─────────────────────┴──────────────────────────────────────────────────────────┘ SQLiteData (private.db) willFetchChanges SQLiteData (private.db) stateUpdate SQLiteData (private.db) didFetchChanges SQLiteData (private.db) willSendChanges SQLiteData (private.db) sentRecordZoneChanges ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Saved │ reminderTags │ 2b267d92-4712-4ad8-b2d7-972cb479b910:reminderTags │ │ ✔︎ Saved │ reminders │ 5c1a349d-5078-49c6-bfdd-4ef2d56a1c79:reminders │ │ ✔︎ Saved │ remindersListAssets │ 25ddf013-186f-4c90-b72c-d5e9869afbbf:remindersListAssets │ │ ✔︎ Saved │ remindersLists │ 25ddf013-186f-4c90-b72c-d5e9869afbbf:remindersLists │ └─────────┴─────────────────────┴──────────────────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) didSendChanges SQLiteData (private.db) didSendChanges SQLiteData (private.db) stateUpdate
— 15:26
Next I’ll swipe on the new list to reveal the share action, and tap the share button. This brings up the standard iCloud sharing sheet. It allows you to share with particular people by tapping the “Share With More People” button. Alternatively you can drill down to the “Share Options” screen and generate a link that anyone can use to join this reminders list. You can also customize whether participants will have read and write access to this reminders list, or just read access. And you can even make it so that participants can invite others to join the list, or you can restrict it so that only you, the owner of the reminders list, can invite others.
— 16:21
SQLiteData’s sharing tools support all of these features, but for now let’s keep things simple. I am just going to invite you Stephen by sending you a text message…
— 16:26
A moment later we will see some logs output from iCloud that lets us know what just happened: SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchChanges SQLiteData (private.db) willFetchRecordZoneChanges: co.pointfree.SQLiteData.defaultZone SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ cloudkit.share │ share-89c0e652-d5fa-4995-aa5e-4bb605259e28:remindersLists │ │ ✔︎ Modified │ remindersLists │ 89c0e652-d5fa-4995-aa5e-4bb605259e28:remindersLists │ └────────────┴────────────────┴───────────────────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchRecordZoneChanges ✔︎ Zone: co.pointfree.SQLiteData.defaultZone:__defaultOwner__ SQLiteData (private.db) didFetchChanges
— 16:46
The act of sharing the reminders list in iCloud caused two records to be synchronized to our device, and these records were received from the private CloudKit database. And the reason these logs are still associated with the private database rather than the shared database is because we are the ones doing the sharing. If we were receiving a record that was shared from another user, then we would see the logs coming from the shared DB, and we will see this soon enough.
— 17:26
One of the records received from iCloud is just the updated reminders list. Nothing about the list actually changed, but the act of sharing does count as an “update” and so we receive the freshest copy from the server. The other record we received is unlike any other kind of record we have seen in our logs. Its recordType is cloudkit.share , and it’s recordName is prefixed with share and then the record name of the shared record. This is a special kind of record created by our library when we shared the reminders lists, and it represents the fact that we are currently sharing this reminders list with other users. We can even use this record to get the names and permissions of all participants of the share, and we can use that info to customize how this reminders list is displayed in the root view. But that is something we will get into later.
— 18:23
If we were to reopen the sharing options for the same reminders list and make a change, such as generate a public link for anyone to be able to join our reminders list, we will immediately see changes get synchronized with iCloud: SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchChanges SQLiteData (private.db) willFetchRecordZoneChanges: co.pointfree.SQLiteData.defaultZone SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ cloudkit.share │ share-89c0e652-d5fa-4995-aa5e-4bb605259e28:remindersLists │ │ ✔︎ Modified │ remindersLists │ 89c0e652-d5fa-4995-aa5e-4bb605259e28:remindersLists │ └────────────┴────────────────┴───────────────────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchRecordZoneChanges ✔︎ Zone: co.pointfree.SQLiteData.defaultZone:__defaultOwner__ SQLiteData (private.db) didFetchChanges SQLiteData (private.db) willSendChanges SQLiteData (private.db) didSendChanges SQLiteData (private.db) stateUpdate
— 18:49
So any changes we make to the share is automatically synchronized to iCloud, and subsequently will also be pushed out to all other devices and users so that they have the most up-to-date details.
— 19:03
We can also stop sharing the reminders list by opening up the sharing details, and tapping the “Stop Sharing” button. After confirming the share sheet will be dismissed, and then in the iCloud logs we will see the following: SQLiteData (private.db) stateUpdate SQLiteData (private.db) willSendChanges SQLiteData (private.db) didSendChanges SQLiteData (shared.db) willSendChanges SQLiteData (shared.db) didSendChanges SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ remindersLists │ 89c0e652-d5fa-4995-aa5e-4bb605259e28:remindersLists │ │ ⌫ Deleted │ cloudkit.share │ share-89c0e652-d5fa-4995-aa5e-4bb605259e28:remindersLists │ └────────────┴────────────────┴───────────────────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchRecordZoneChanges ✔︎ Zone: co.pointfree.SQLiteData.defaultZone:__defaultOwner__ SQLiteData (private.db) didFetchChanges
— 19:18
The special cloudkit.share record has been deleted, and this deletion would also be sent all devices of all participants of the share, and that will cause the shared reminders list to also be deleted from those devices.
— 19:42
But, I really do want to share this list with you so I am going to open the share settings for the “Point-Free” list again, and invite you to the list via a text message:
— 19:56
And a moment later we get the logs that prove this record has been shared. Receiving a shared record
— 20:06
OK, we now have the basics of sharing a reminders list with another iCloud user. It took remarkably little code. We just needed to add a button for the user to tap, and then in the action closure of that button we invoke the share(record:) method on the sync engine to get a shared record, and finally we used that shared record to drive the presentation of a share sheet. Stephen
— 20:29
But sharing a record is only half the story. We need to do a little work in order to allow a user to accept a shared record. When you just shared the “Point-Free” reminders list with me I got a text message, and in that message was a link. Tapping on that link will open our app up, but that won’t magically accept the invitation and start synchronizing the data to our device. We need to implement some scene delegate methods to be notified of when the invitation link is opened, and then from there we can accept the shared record.
— 20:56
Let’s check it out.
— 21:00
At the entry point of the app we need to define a UIWindowSceneDelegate class so that we can receive various lifecycle events from our app’s scenes. A minimal conformance can be just this: class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? }
— 21:22
And then to use this scene delegate we need a UIApplicationDelegate conformance that uses this new SceneDelegate object as the delegate for the app. The minimal conformance for this is the following: class AppDelegate: UIResponder, UIApplicationDelegate { func application( _ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions ) -> UISceneConfiguration { let configuration = UISceneConfiguration( name: "Default Configuration", sessionRole: connectingSceneSession.role ) configuration.delegateClass = SceneDelegate.self return configuration } }
— 21:40
And then we can install this app delegate into the entry point of the app like so: @main struct RemindersApp: App { @UIApplicationDelegateAdaptor var delegate: AppDelegate … }
— 21:53
With that done our AppDelegate and SceneDelegate objects will receive all app lifecycle events.
— 21:57
There are two main methods that need to be implemented in the scene delegate. One method is called when your app is already running in the background and your user taps on a share invitation link: func windowScene( _ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith cloudKitShareMetadata: CKShare.Metadata ) { } And we need to import CloudKit to get access to some of these symbols: import CloudKit
— 22:23
But if your app is not currently running in the background and instead it is launched fresh from your user tapping on a share invitation link, then a different method is invoked: func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { }
— 22:34
And the connectionOptions value can be inspected to determine if this launch is happening due to accepting a share invitation or not.
— 22:42
We will perform very similar work in each of these methods. Let’s start with the first one. In this method we are handed the cloudKitShareMetadata that represents the share we want to accept. There is a special method on the SyncEngine that allows you to accept the share from this metadata, but we first need access to the sync engine so let’s add it as a dependency to the delegate: class SceneDelegate: UIResponder, UIWindowSceneDelegate { @Dependency(\.defaultSyncEngine) var syncEngine … }
— 23:07
And then we can invoke the acceptShare(metadata:) method and pass along the metadata: syncEngine.acceptShare(metadata: cloudKitShareMetadata)
— 23:20
This method is async and so we really have no choice but to spin up an unstructured task: Task { await syncEngine.acceptShare(metadata: cloudKitShareMetadata) }
— 23:32
And this method can throw. This time any errors thrown are going to be due to CloudKit problems, and so it would probably be best to notify the user in some way, but we aren’t going to do that now: Task { do { try await syncEngine.acceptShare(metadata: cloudKitShareMetadata) } catch { // TODO: let user know CloudKit is having problems and to try again } }
— 23:49
The next scene delegate method can be implemented in much the same way, but we have to extract the CloudKit metadata from the connectionOptions first: func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { guard let cloudKitShareMetadata = connectionOptions .cloudKitShareMetadata else { return } Task { do { try await syncEngine.acceptShare( metadata: cloudKitShareMetadata ) } catch { // TODO: Let user know CloudKit is having problems } } }
— 24:10
And, well, that’s it! That’s all it takes to allow our users to accept share invitations. It may seem unbelievable but with just a few dozen lines of code we have fully hooked up reminders list sharing in this app.
— 24:22
To see all of this working, I am going to install the app on my device, and after a few moments we will see a “Point-Free” list appear, and in the logs we can see exactly what happened: SQLiteData (shared.db) willFetchChanges SQLiteData (shared.db) fetchedDatabaseChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ zoneName ┃ ownerName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ co.pointfree.SQLiteData.defaultZone │ _0d6f10647d556fd62e3dc6abca9ee25d │ └────────────┴─────────────────────────────────────┴───────────────────────────────────┘ SQLiteData (shared.db) stateUpdate SQLiteData (shared.db) willFetchRecordZoneChanges: co.pointfree.SQLiteData.defaultZone SQLiteData (shared.db) stateUpdate SQLiteData (shared.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ cloudkit.share │ share-2473930a-391d-4b45-a72e-3043e5e45f93:remindersLists │ │ ✔︎ Modified │ reminders │ d15e25c8-c03d-4c3f-bb9d-246916ab5c00:reminders │ │ ✔︎ Modified │ remindersListAssets │ 2473930a-391d-4b45-a72e-3043e5e45f93:remindersListAssets │ │ ✔︎ Modified │ remindersLists │ 2473930a-391d-4b45-a72e-3043e5e45f93:remindersLists │ └────────────┴─────────────────────┴───────────────────────────────────────────────────────────┘ SQLiteData (shared.db) stateUpdate SQLiteData (shared.db) willFetchRecordZoneChanges ✔︎ Zone: co.pointfree.SQLiteData.defaultZone:_0d6f10647d556fd62e3dc6abca9ee25d SQLiteData (shared.db) didFetchChanges SQLiteData (shared.db) didFetchChanges SQLiteData (shared.db) willSendChanges SQLiteData (shared.db) didSendChanges SQLiteData (shared.db) stateUpdate
— 24:56
There a few interesting things in these logs. First, we get a log for a some changes saved to a record zone: SQLiteData (shared.db) fetchedDatabaseChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ zoneName ┃ ownerName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ co.pointfree.SQLiteData.defaultZone │ _0d6f10647d556fd62e3dc6abca9ee25d │ └────────────┴─────────────────────────────────────┴───────────────────────────────────┘
— 25:00
This zone is different from the default zone we’ve encountered so far. This is actually your zone from your device. Since records from your database are being shared with me I now get notified of changes to your zone. And it’s very important for the internals of our SQLiteData library to keep track of this zone and properly create new records in this zone when appropriate, otherwise we run the risk of getting CloudKit errors and losing data.
— 25:22
This is also the first time we are seeing logs from the shared iCloud database. This is happening because these records are not actually stored in my iCloud database. They are stored in your database, and I am just getting access to them.
— 25:32
Another incredible thing we are seeing here is that we not only received a record from CloudKit for the “Point-Free” list: SQLiteData (shared.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ cloudkit.share │ share-2473930a-391d-4b45-a72e-3043e5e45f93:remindersLists │ │ ✔︎ Modified │ reminders │ d15e25c8-c03d-4c3f-bb9d-246916ab5c00:reminders │ │ ✔︎ Modified │ remindersListAssets │ 2473930a-391d-4b45-a72e-3043e5e45f93:remindersListAssets │ │ ✔︎ Modified │ remindersLists │ 2473930a-391d-4b45-a72e-3043e5e45f93:remindersLists │ └────────────┴─────────────────────┴───────────────────────────────────────────────────────────┘ …but we also received a record for the associated reminders list asset and the reminder we created:
— 25:47
And indeed I can see the “Point-Free” list in the root view of all of my reminders lists. And if I drill down to that list I will see the cover image, as well as the reminder.
— 25:54
This is incredible because we didn’t do any extra work to share these associated records. The SQLiteData library analyzed the foreign key relationships between our models and used that information to figure out how to share all the records associated with our “Point-Free” reminders list.
— 26:07
One thing to note here is that it doesn’t seem like the “reminderTag” that you assigned to your reminder has been shared with me. I don’t see it mentioned in the logs and I don’t see it in the UI. There is a very good reason for this, but we will talk about that in a bit.
— 26:19
And to really show off that all of this data is truly being shared between my iCloud account and your iCloud account, we are going to do something we’ve never done on Point-Free before…
— 26:29
We are switching to a simulcast of both of our devices on screen at once so that we can really see that we are live collaborating on this reminders list. Let’s start by both drilling into the “Point-Free” reminders list. I am going to complete the “Send weekly emails” reminder that you created a moment ago:
— 26:59
And a moment later we will see iCloud logs showing that our changes were saved to the shared database, and then a moment later you should see the reminder disappear from your device. Brandon
— 27:08
And I do! And to see that it really was just completed and not deleted, I can show all completed reminders in this list and we will see it reappear. Stephen
— 27:17
I really can’t stress enough how incredible this is. We have only added a few lines of code to our app in order to unlock collaboration on records with other iCloud users. And crucially, we have yet to make a single change to our core reminders logic to support this functionality. In fact, we didn’t even build this app with synchronization or sharing in mind. We only bolted it on after the fact. Most everything we have built in previous episodes has remained unchanged, and if we wanted to add a new feature to our app we wouldn’t have to think about how are we going to support iCloud synchronization and sharing. It’s all just done for us magically behind the scenes.
— 27:57
And all of the amazing things we showed off in the previous 2 episodes work equally well when sharing records over iCloud. For example, conflict resolution works the same way we previously have demonstrated, even when two different iCloud users are collaborating on the same record. I’m going to start by un-completing the “Send weekly emails” reminder, and a moment later it should re-appear on your device.
— 28:22
And now let’s both open the details of this reminder. I am going to edit the title to say “Send weekly email and social media posts”. Brandon
— 28:34
And I will edit the same reminder to make the due date for next Monday and I am going to mark it as high priority. Stephen
— 28:44
Now let’s both hit save at the same time. After a moment we will each see some logs fly by showing us what was sent to and received from iCloud. But ultimately both of our devices will settle into a state where the reminder’s title has been updated to “Send weekly email and social media posts”, the due date is next Monday, and the reminder is marked as high priority.
— 29:07
So, even though we both made edits at the same time to this one record, all edits were kept because we edited different fields. If we had edited the same field then SQLiteData would have simply chosen the field that was edited most recently. We find that this form of effortless conflict resolution works just fine for many kinds of apps out there.
— 29:26
Further, even if both of our devices were running different versions of the app’s schema we would still be able to collaborate on this reminders list. For example, you could be running a version of the app that predates the URL feature we added for reminders. In that situation we would still be able created and edit reminders even though each of our devices have different schemas. We aren’t going to show this off right now, but rest assured it would work.
— 29:49
Another thing that just works is assets. Brandon
— 29:51
And I can go back to the details of the “Point-Free” reminders list, I’ll add a new cover image. Before I hit save make sure you drill back into the “Point-Free” list, and then I will hit save. A moment later you should see the image I chose. Stephen
— 30:17
And it did!
— 30:24
There’s another tricky situation we want to show off here. Because we haven’t made any changes to the core logic of our app to take into consideration sharing, it is the case that I can back up to the root view, swipe on the “Point-Free” reminders list, and I do have a delete button available to me.
— 30:42
What would happen if I deleted this list? Will it delete it for you even though you are the owner of the list? Or will it just cause me to be removed from the shared list so that I no longer have access to it?
— 30:53
Well, this turns out to be surprisingly nuanced and complex to handle from within the SQLiteData, but as a user of the library you don’t have to worry about any of that nuance or complexity. I can simply delete the record, and the library takes care of understanding that we can’t actually delete the record from CloudKit, and doing so would actually be an error. But instead it deletes the share , and eventually everything synchronizes to remove the list from your device: SQLiteData (shared.db) sentRecordZoneChanges ┏━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordName ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ⌫ Deleted │ share-2473930a-391d-4b45-a72e-3043e5e45f93:remindersLists │ └───────────┴───────────────────────────────────────────────────────────┘
— 31:21
So we now no longer have access to that shared list, but it did not delete it from your device. Brandon
— 31:26
That’s right, I still have access to my “Point-Free” list. Stephen
— 31:38
And this nuance and complexity also goes in the other direction. That is, if you, the owner of the reminders list, deletes the reminders list, then under the hood SQLiteData takes care to also delete the underlying CloudKit share so that the record is removed from all participants’ devices.
— 31:52
To see this, I am going to re-join your reminders list so that we can see what happens when you delete it sometime later. Brandon
— 32:05
OK I have done that, and if I pop back to the root I can swipe and delete the list, and a moment ago the list should disappear for you. Stephen
— 32:20
It did! And in my logs I can see that the entire shared zone was deleted, and that caused the list and all associated records to be deleted.
— 32:35
This too is a surprisingly tricky situation to get right in the library. It required a lot of difficult work and testing to make sure we properly figure out what to do when shared records are deleted. But the end result of all of that hard work is that our library users don’t have to think about it. They can simply delete records as they see fit, and SQLiteData takes care of the hard stuff. Next time: Sync metadata
— 32:54
I know I’m sounding like a broken record, but it’s incredible that we were able to add a feature to our reminders app that allows us to collaborate on a reminders list. We can both create, edit and delete reminders, and we can even both edit the cover image for the list.
— 33:07
It only took a few lines of code, it was bolted onto an app that was not specifically built to support data sharing or even iCloud synchronization, and it just magically works. Our SQLiteData library is handling a ton of edge cases and nuances having to do with conflict resolution, tracking record zones, and listening for changes in both the private and shared iCloud databases at the same time. Brandon
— 33:27
Building this kind of feature used to be the kind of thing we would dread. We could have spent months building a wonderful app for our users, and the moment we release it to the world we get back the first bit of feedback from our users, which is that they want their data synchronized to all their devices, and they want to share some of their data with friends or family. Well, we no longer have to dread this kind of feature request. SQLiteData takes care of all of that complexity for you, and you can focus your attention on just building your features in basically the same way you would have prior to worrying about sync and sharing. Stephen
— 34:04
And it’s worth mentioning that while SwiftData does have rudimentary support for iCloud synchronization, it does not support iCloud sharing at all. This means if you use SwiftData to handle persistence in your app, you simply will not be able to support allowing your users to share bits of their data with other iCloud users. You will have to wait with bated breath for each WWDC to roll around and hope that it eventually gets added. Brandon
— 34:26
And you might think that seamless iCloud data sharing would have been the ultimate cherry on top of this series of episodes, but we have an additional, tiny cherry to put on top of the existing cherry. We went above and beyond when building these tools because we wanted them to work seamlessly behind the scenes without you having to worry about how they work, but we also wanted to make sure you had full access to everything happening under the hood.
— 34:53
In particular, while you, the user of our library, gets to simply deal with SQLite databases and struct data types representing your tables, our library has to translate all of that information into CKRecord s to send out to CloudKit. And conversely, when we receive CKRecord s from CloudKit we have to update your SQLite database. You typically don’t have to think about these CKRecord s, but sometimes it can be handy to get access to them. They contain information that can be useful and that you don’t have access to from just your own SQLite database alone.
— 35:29
So, let’s see where this data is stored, what all is stored, and let’s see how it can be useful to customize our applications that make use of iCloud synchronization and sharing…next time! References SQLiteData Brandon Williams & Stephen Celis A fast, lightweight replacement for SwiftData, powered by SQL. https://github.com/pointfreeco/sqlite-data StructuredQueries A library for building SQL in a safe, expressive, and composable manner. https://github.com/pointfreeco/swift-structured-queries Downloads Sample code 0343-sync-pt4 Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .