EP 350 · Tour of SQLiteData · Jan 12, 2026 ·Members

Video #350: Tour of SQLiteData: CloudKit

smart_display

Loading stream…

Video #350: Tour of SQLiteData: CloudKit

Episode: Video #350 Date: Jan 12, 2026 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep350-tour-of-sqlitedata-cloudkit

Episode thumbnail

Description

We conclude our tour by adding iCloud synchronization and collaborative sharing, all in under thirty minutes! We will show how support will not require any fundamental changes to our application, show off live synchronization across multiple devices and users, and we will use our upcoming “Point-Free Way” skill documents to let Xcode’s Coding Assistant write things for us.

Video

Cloudflare Stream video ID: e9c6917ec1bf10e9752b0478ba240c6e Local file: video_350_tour-of-sqlitedata-cloudkit.mp4 *(download with --video 350)*

References

Transcript

0:05

Alright, we now have a fully functional score keeper app! We can add and delete games, within each game we can add and delete players, and even sort the players. And we are able to assign photos to each player. All of the data is automatically persisted to disk so that it’s available next launch, and each step of the way we saw that our SQLiteData library gives us powerful tools for querying our data that can be used in SwiftUI views as well as observable models. Stephen

0:32

But now let’s get on to the part that I know everyone is most interested in: synchronization! We want our users to have access to their data across all of their devices, and we want to do it in such a way that we don’t really have to think about it when making new features. We should be able to add new screens to our app that insert data into the database, delete data, query for data, all without thinking about synchronization, and it should all just work magically.

0:56

Well, SQLiteData makes iCloud synchronization possible, and it handles it seamlessly. Let’s take a look. iCloud configuration

1:04

In order to start synchronizing our data to iCloud we need to create and configure a SyncEngine . It’s the object that is responsible for observing all changes in your database and uploading that data to iCloud.

1:13

To do this, let’s give our “Point-Free Way” documents another run. The SQLiteData.md file contains detailed information on how to set up a sync engine, and so let’s ask Xcode to do so: Prompt Add a sync engine to this app for all of my tables.

1:35

And amazing it generates the correct code. It suggests we overwrite the defaultSyncEngine dependency in our bootstrapDependencies() function, and provide it our database connection as well as a list of the tables we want to sync: extension DependencyValues { mutating func bootstrapDatabase() throws { defaultDatabase = try appDatabase() defaultSyncEngine = try SyncEngine( for: defaultDatabase, tables: Game.self, Player.self, PlayerAsset.self ) } }

2:00

It even gives us a summary of some next steps which are actually quite helpful: Next steps Make sure iCloud capabilities and entitlements are configured for your app and targets. …

2:04

That first bullet point is particularly helpful. Before we can sync anything to iCloud we do indeed need to set up our project properly. To do this we go to the project settings, then the “Signing & Capabilities” tab, and add an iCloud entitlement. In that entitlement we will enable the “CloudKit” service and add a container to be used. Then we will add a “Background modes” entitlement and enable the “Remote notifications” setting. All of those steps make it possible for our app to start sending data to iCloud as well as receive data from iCloud.

2:57

There are additional arguments on the SyncEngine that may be of interest to you depending on the type of app you are building:

3:04

The privateTables argument allows you to specify which tables should not be shared with other users, even if they are associated with another shared table. This can be handle for data that is not appropriate to share, such as a user’s preferences for sorting and filtering the rows in a list.

3:17

The startsImmediately argument allows you to start a sync engine in a state where it is not actively running and sync’ing data. You may want to this if you want to gate synchronization behind some user action, such as them turning it on explicitly, or them paying for that functionality.

3:22

And the logger argument allows you to provide a custom logger to be used by the sync engine, or disable logging entirely.

3:24

We don’t currently need any of this functionality, but it’s worth noting that SwiftData does not provide any means to customize these kinds of things. First of all, SwiftData doesn’t support record sharing at all, and it doesn’t support conditionally turning sync on or off. And it also doesn’t log what is happening inside the sync engine, which as we will soon see is extremely useful.

3:41

That is all it takes to start instantly sending records to iCloud in order to sync the data to all devices. However, if we run the app in the simulator we will get flooded with warnings like this: <CKSyncEngine Private> error updating account info: Error Domain=CKErrorDomain Code=1 “Account signed in, but we don’t have the current userRecordID to create an event” UserInfo={NSLocalizedDescription=Account signed in, but we don’t have the current userRecordID to create an event} Error fetching user record ID: <CKUnderlyingError 0x600000c3b7e0: "AccountUnavailableDueToBadAuthToken" (1029/2011); "Account temporarily unavailable due to bad or missing auth token"> Warning: SyncEngine error updating userRecordID: <CKError 0x600000c3bab0: "Account Temporarily Unavailable" (36/1029); "Account temporarily unavailable due to bad or missing auth token"> Warning: <CKSyncEngine Shared> error fetching changes for context <FetchChangesContext reason=scheduled options=<FetchChangesOptions scope=all group=CKSyncEngine-FetchChanges-Manual groupID=8759C4A57DC8C862)>> : <CKError 0x600000c08720: “Not Authenticated” (9); “Could not determine iCloud account status”>` The errors are pretty clear on what is wrong.

4:05

We just need to go over to the settings app in the simulator, and log into our iCloud account…

4:14

Now when we go back to the score keeper app we will see lots of logs fly by, but none of them seemed to be warnings or errors. And if we filter the logs by the “SQLiteData” process, we will see exactly what the sync engine did under the hood: Metadatabase connection: open "…/Library/Developer/CoreSimulator/…/.SQLiteData.metadata-iCloud.co.pointfree.Scorekeeper.sqlite" SQLiteData (shared.db) signIn Current user: _a98a5ec3eebebc170910d929118722a7.__defaultOwner__._defaultZone SQLiteData (shared.db) stateUpdate SQLiteData (private.db) stateUpdate SQLiteData (private.db) signIn Current user: _a98a5ec3eebebc170910d929118722a7.__defaultOwner__._defaultZone SQLiteData (private.db) stateUpdate SQLiteData (private.db) stateUpdate SQLiteData (shared.db) willFetchChanges SQLiteData (private.db) willFetchChanges SQLiteData (private.db) stateUpdate SQLiteData (shared.db) stateUpdate SQLiteData (private.db) stateUpdate SQLiteData (private.db) didFetchChanges SQLiteData (shared.db) stateUpdate SQLiteData (shared.db) didFetchChanges SQLiteData (private.db) willSendChanges SQLiteData (shared.db) willFetchChanges SQLiteData (shared.db) stateUpdate SQLiteData (shared.db) didFetchChanges SQLiteData (shared.db) willSendChanges SQLiteData (shared.db) didSendChanges SQLiteData (shared.db) willSendChanges SQLiteData (shared.db) stateUpdate SQLiteData (shared.db) didSendChanges SQLiteData (private.db) willFetchChanges SQLiteData (private.db) stateUpdate SQLiteData (private.db) didFetchChanges SQLiteData (private.db) willSendChanges SQLiteData (private.db) sentDatabaseChanges ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ zoneName ┃ ownerName ┃ ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Saved │ co.pointfree.SQLiteData.defaultZone │ __defaultOwner__ │ └─────────┴─────────────────────────────────────┴──────────────────┘ SQLiteData (private.db) nextRecordZoneChangeBatch: scheduled ┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ event ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ → Sending │ games │ 3f532992-2c4f-4ef4-925f-c8ab57fc3a2a:games │ │ → Sending │ games │ b1cb5a15-b500-4fd8-aaee-6dee3f0ef516:games │ │ → Sending │ games │ a590b2aa-9607-4624-9748-051ff2fb8695:games │ │ → Sending │ players │ e6432703-de8c-4ff4-a648-28ffc6005c65:players │ │ → Sending │ players │ ece276b3-7c14-4c06-980c-da91305d6481:players │ │ → Sending │ players │ 2392887d-9471-4206-9247-b9fa3bf7b84e:players │ └───────────┴────────────┴──────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) sentRecordZoneChanges ┏━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Saved │ games │ b1cb5a15-b500-4fd8-aaee-6dee3f0ef516:games │ │ ✔︎ Saved │ games │ 3f532992-2c4f-4ef4-925f-c8ab57fc3a2a:games │ │ ✔︎ Saved │ games │ a590b2aa-9607-4624-9748-051ff2fb8695:games │ │ ✔︎ Saved │ players │ e6432703-de8c-4ff4-a648-28ffc6005c65:players │ │ ✔︎ Saved │ players │ ece276b3-7c14-4c06-980c-da91305d6481:players │ │ ✔︎ Saved │ players │ 2392887d-9471-4206-9247-b9fa3bf7b84e:players │ └─────────┴────────────┴──────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) didSendChanges SQLiteData (private.db) didSendChanges SQLiteData (private.db) stateUpdate

4:30

We can clearly see that it send all our local data to iCloud, including games, players and assets, and then a moment later we got a response back from iCloud that everything was saved successfully. And this data really is stored on iCloud so that if we installed the app on some other device, like an iPad, or we purchased a new iPhone and installed everything from scratch, all of this data would immediately come flooding in when we first open it.

4:50

To emulate this, let’s delete the app from the simulator, and re-install it from scratch. At first the screen is blank, but after a moment the data will appear, and in the logs we can see that the sync engine successfully loaded all of the games, players and assets: SQLiteData (private.db) fetchedDatabaseChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ zoneName ┃ ownerName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ co.pointfree.SQLiteData.defaultZone │ __defaultOwner__ │ └────────────┴─────────────────────────────────────┴──────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchRecordZoneChanges: co.pointfree.SQLiteData.defaultZone SQLiteData (private.db) stateUpdate SQLiteData (private.db) sentDatabaseChanges ┏━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ zoneName ┃ ownerName ┃ ┡━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Saved │ co.pointfree.SQLiteData.defaultZone │ __defaultOwner__ │ └─────────┴─────────────────────────────────────┴──────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ games │ 3f532992-2c4f-4ef4-925f-c8ab57fc3a2a:games │ │ ✔︎ Modified │ games │ b1cb5a15-b500-4fd8-aaee-6dee3f0ef516:games │ │ ✔︎ Modified │ games │ a590b2aa-9607-4624-9748-051ff2fb8695:games │ │ ✔︎ Modified │ players │ e6432703-de8c-4ff4-a648-28ffc6005c65:players │ │ ✔︎ Modified │ players │ ece276b3-7c14-4c06-980c-da91305d6481:players │ │ ✔︎ Modified │ players │ 2392887d-9471-4206-9247-b9fa3bf7b84e:players │ └────────────┴────────────┴──────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate

5:30

So this does show we are syncing data to iCloud because that is the only way this data could magically flow back after deleting the app.

5:37

But it doesn’t show the synchronization process happening in real time. To see that we need to run the app on multiple devices, which we will do now by going into a split screen of of my computer screen on the left, and my actual, personal iPhone device on the right. I am now going to install our score keeper app on my personal iPhone, and we will see after a moment all of the data comes flowing in.

6:02

Now let’s do some fun things. In the simulator I am going to add a new game, say “Little league baseball”, and I will add two players to the game: the “Tigers” and the “Bluejays”. And before I could even finish doing all of that my phone was already receiving this data from iCloud. First the game appeared, and then a moment later the count in the row increased to 2. This shows that even though we are not directly showing players on this games list, the fact that new players were added made the view automatically refresh so that it could display the freshest data.

6:45

Now suppose I delete this new game from my device. That has also deleted all of the players from the database, and after a moment we can see that event was transmitted to iCloud: SQLiteData (private.db) sentRecordZoneChanges ┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ⌫ Deleted │ │ 3a3aaacc-eefd-4a0b-884e-186bb3e4f6cc:players │ │ ⌫ Deleted │ │ 5b5778f6-bfd3-4f48-915a-ccb138e3ce4e:games │ │ ⌫ Deleted │ │ 68a5b263-649a-4923-b766-7dd6cc3ab855:players │ └───────────┴────────────┴──────────────────────────────────────────────┘

7:00

However, the simulator does not seem to be updating. This is just because simulators do not receive background notifications like devices do, and so they don’t get automatic updates. A way to kick the simulator in the butt is to background the app, and then re-open the app, and now we see the game and players removed. iCloud Sharing

7:41

So we really are synchronizing data back and forth on these devices. And the library even handles conflict resolution for you automatically. If the same record is edited on two devices before they can synchronize with each other, then those changes are merged in a “last edit wins” strategy applied to each field. This means that if there is no overlap in the fields edited by each device then the data will merge cleanly with no loss. Only if two devices edit the same field will there be an actual conflict, at which point whichever field was edited must recently will be the one chosen.

8:14

It’s worth noting that SwiftData does not employ this kind of conflict resolution. It does “last edit wins” too, but on the full record. This means if two devices each edit completely different fields from one another, once the data syncs all of the changes from one device will be thrown out in favor of the other device. Brandon

8:30

OK, we have now seen that with basically one line of code we are now synchronizing our database across multiple devices. It happens transparently for us behind the scenes, it also has a lot of customization points that SwiftData does not have, and it even has a more robust conflict resolution strategy.

8:50

Now suppose that our humble little score keeper has been out in the world for a few weeks, and people are downloading it and actually using it. But then we get a request from someone that they would love to be able to share a game with a friend so that they can collaborate on a game’s scores together. That seems like a very reasonable request, but how on earth can we hope to build such a feature?

9:14

Well, SQLiteData makes this quite easy to do! Let’s take a look.

9:19

We already have a button in place that we want the user to be able to tap on in order to share the game they are viewing: Button { <#Action#> } label: { Image(systemName: "square.and.arrow.up") }

9:29

We need to actually implement the logic for this button, and we have ample docs that describe exactly how to do this. It doesn’t take too many steps, but we have also meticulously described those steps in the “Point-Free way” docs, and so let’s try asking Xcode to implement this feature for us: Prompt Implement the logic and behavior to share a game with another iCloud user when the share button in the toolbar is tapped

10:20

It has started to explain what code it wants to add, and we have a few samples of that code. It looks like it wants to add some new state to the model: var sharedRecord: SharedRecord?

10:42

This is the state that will drive the presentation of a share sheet so that the user can decide who they want to share the game with. Next it suggests adding a sync engine dependency to the model: @ObservationIgnored @Dependency(\.defaultSyncEngine) var syncEngine This will be necessary to create a shared record.

10:53

Then it suggests adding a method to the model for when the shared button is tapped, and it mutates the newly added state based on what is returned from the share(record:) method on the sync engine: func shareButtonTapped() async { await withErrorReporting { sharedRecord = try await syncEngine.share(record: game) } }

11:16

Then it recommends invoking this async method from the view by spinning up an unstructured task: Button { Task { await model.shareButtonTapped() } } label: { Image(systemName: "square.and.arrow.up") }

11:30

And finally suggests driving the presentation of a sheet based on this newly created state: .sheet(item: $model.sharedRecord) { sharedRecord in CloudSharingView(sharedRecord: sharedRecord) }

11:46

And amazingly, this is all correct. But, it is only half the story for sharing a record over iCloud. In the prompt output it notes that to allow accepting shared records in the app we have to added an entry to our info plist, and we have customize the entry point of our app to be notified when the app is opened due to accepting a shared record.

12:11

We first need to add this entry to our app’s plist by hand. Xcode’s assistant doesn’t seem to edit non-code files for us: <key>CKSharingSupported</key> <true/>

12:23

And then we can ask the assistant to do some actual coding: Prompt Update our app entry point to properly handle iCloud sharing.

12:59

And incredibly it takes care of all the details. It adds the following app delegate and scene delegate to the entry point: import CloudKit import Dependencies import SQLiteData import UIKit 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 } } class SceneDelegate: UIResponder, UIWindowSceneDelegate { @Dependency(\.defaultSyncEngine) var syncEngine var window: UIWindow? func windowScene( _ windowScene: UIWindowScene, userDidAcceptCloudKitShareWith metadata: CKShare.Metadata ) { Task { try await syncEngine.acceptShare(metadata: metadata) } } func scene( _ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions ) { guard let metadata = connectionOptions.cloudKitShareMetadata else { return } Task { try await syncEngine.acceptShare(metadata: metadata) } } } And it used this app delegate in the SwiftUI @main entry point like so: @main struct ScorekeeperApp: App { @UIApplicationDelegateAdaptor private var appDelegate: AppDelegate … }

13:58

And incredibly that is actually all it takes to implement iCloud sharing. It’s difficult to test this on just one device with one single iCloud account, but let’s see what it looks like real quick. I am going to install this on my own personal iPhone…

14:33

There it is. And I am going to create to a game and a few players, tap the share icon in the top right, and a moment later a share sheet comes flying up. I can share this record in a few different ways, but to keep things simple I will simply text a link to you, Stephen.

15:45

That text has been sent, and if we check the SQLiteData logs we will see that a special share record has been created: SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ cloudkit.share │ share-3f532992-2c4f-4ef4-925f-c8ab57fc3a2a:games │ │ ✔︎ Modified │ games │ 3f532992-2c4f-4ef4-925f-c8ab57fc3a2a:games │ └────────────┴────────────────┴──────────────────────────────────────────────────┘

15:59

Now Stephen should have received a text message inviting them to this game, and just to be sure, let’s bring Stephen on camera… Stephen

16:05

I’ve already got the freshest version of the app running on my devices, and I am currently in my text messages where I have received a text from you that is inviting me to your shared game. By tapping that link the Scorekeeper app automatically opens and I see the shared “Weekly poker night” game. Brandon

16:30

And further, I can see that you have accepted the share from looking at the logs on my device: SQLiteData (private.db) willFetchChanges SQLiteData (private.db) stateUpdate SQLiteData (private.db) fetchedDatabaseChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ zoneName ┃ ownerName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ co.pointfree.SQLiteData.defaultZone │ __defaultOwner__ │ └────────────┴─────────────────────────────────────┴──────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchRecordZoneChanges: co.pointfree.SQLiteData.defaultZone SQLiteData (private.db) stateUpdate SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ cloudkit.share │ share-3f532992-2c4f-4ef4-925f-c8ab57fc3a2a:games │ └────────────┴────────────────┴──────────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchRecordZoneChanges ✔︎ Zone: co.pointfree.SQLiteData.defaultZone:__defaultOwner__ SQLiteData (private.db) didFetchChanges

16:38

This “cloudkit.share” record is a special record sync’d across devices that represents the share we created. Stephen

16:58

OK, now let me try to add an avatar to my player. Brandon

17:07

If I wait a moment I will see my list of players update, and I see your updated avatar. And I can see in the logs that I received a modified record from CloudKit: SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ playerAssets │ ece276b3-7c14-4c06-980c-da91305d6481:players │ └────────────┴──────────────┴──────────────────────────────────────────────┘

17:16

Next let’s make some edits at the same time. I’ll increment the score of a few players while you add a player.

17:37

OK, I saw your “Blob Jr” fly in and get incremented to 1. Stephen

17:44

I see “Blob” leading things with 3 points, I’ve got 2 points, and you still have 0. Brandon

17:53

Let’s also see what happens when one device deletes a player. I’ll delete “Blob” since he’s in first place. Stephen

18:02

And yup, he’s gone. Brandon

18:04

Yep, I just saw the player removed from my list, and I can see in the logs that a player was deleted: SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ⌫ Deleted │ players │ 2392887d-9471-4206-9247-b9fa3bf7b84e:players │ └───────────┴────────────┴──────────────────────────────────────────────┘ Stephen

18:10

So what about deleting the entire game? If I want to bow out I can delete the player and then pop back to the root view and delete the entire game. Brandon

18:27

Yup, I saw your player go away, and if I check the logs I can see that the cloudkit.share record was modified: SQLiteData (private.db) willFetchChanges SQLiteData (private.db) stateUpdate SQLiteData (private.db) fetchedDatabaseChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ zoneName ┃ ownerName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ co.pointfree.SQLiteData.defaultZone │ __defaultOwner__ │ └────────────┴─────────────────────────────────────┴──────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchRecordZoneChanges: co.pointfree.SQLiteData.defaultZone SQLiteData (private.db) stateUpdate SQLiteData (private.db) fetchedRecordZoneChanges ┏━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃ action ┃ recordType ┃ recordName ┃ ┡━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩ │ ✔︎ Modified │ cloudkit.share │ share-3f532992-2c4f-4ef4-925f-c8ab57fc3a2a:games │ └────────────┴────────────────┴──────────────────────────────────────────────────┘ SQLiteData (private.db) stateUpdate SQLiteData (private.db) willFetchRecordZoneChanges ✔︎ Zone: co.pointfree.SQLiteData.defaultZone:__defaultOwner__ SQLiteData (private.db) didFetchChanges But crucially the data was not deleted on my device. All that happened, as far as I can tell, is that Stephen was removed from the shared record.

19:07

And finally, let’s see what happens when I delete the game when it’s shared. First, rejoin the game, and then I will delete it entirely, which should not only unshare it, but also delete it from your device. Stephen

19:52

Yup, it’s gone! Brandon

19:58

This is all pretty incredible, because all of these features were written without sharing in mind, but once we add the ability to share, all of the cross-device synchronization and collaboration just works. Querying metadata

20:39

Alright, this is pretty amazing. I don’t know about you, but before seeing this, I would have dreaded the day that a user of my app requested that they be able to share their games with other users. It’s a totally reasonable request, but it’s typically quite complex to implement.

20:55

And here we have done it in just a few lines of code, and it works perfectly. And we don’t even have to worry about accounts for users or anything like that. All of our users already have iCloud accounts, and we get to leverage all of that infrastructure for free. Stephen

21:08

But not only is it easy to share records with other users for collaboration, but we can actually query the metadata associated with iCloud records and share data from SQLite !

21:18

Let’s take a look a quick, very simple example of this now.

21:22

I have the scorekeeper app running in the simulator, and I have a few games in the root list. I’m going to go to one of these games, and share it by generating a public link. Once that is done I can inspect to the logs to see that indeed the share was created. But there’s no indication back in the root list which of these games are private to just me and which have been shared with others. Let’s make it so that we can put a small icon next to all of the shared games.

22:04

We already have a data type that represents the data needed to display each row. Well, not we have a little bit more data we need, a boolean that determines whether or not the game is shared: struct GamesView: View { @Selection struct Row { … let isShared: Bool … } … }

22:19

We just need to update the query that powers this rows state to somehow compute this state. One thing we can do real quick just to get things compiling is expand the Columns initializer to include isShared: false : Row.Columns( game: $0, isShared: false, playerCount: $1.count() )

22:35

But we still need to compute this data somehow. How can we determine if the game we are querying for is currently being shared?

22:39

Well, behind the scenes, the SQLiteData library manages a table called SyncMetadata that has this information, and a lot more. And we can actually join our query to that table like so: .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) }

23:07

This finds the corresponding sync metadata for each game we fetch in our query, if it exists.

23:12

This table has a column called isShared that determines if the corresponding game is being shared or not: Row.Columns( game: $0, playerCount: $1.count(), isShared: $2.isShared )

23:18

However, there’s a subtlety to be aware of here. Because we are left joining, the sync metadata schema is optionalized since it is possible for a game to not yet have any sync metadata. This means $2.isShared is actually an optional boolean, and so we need to coalesce it to an honest boolean by using the ifnull function from SQL: Row.Columns( game: $0, playerCount: $1.count(), isShared: $2.isShared.ifnull(false) )

23:52

Now this compiles, and we can start making use of this state in the view to show an icon: HStack { if row.isShared { Image(systemName: "network") } … }

24:15

This is nearly all it takes to implement this feature, but if we run it in the simulator we will get a purple runtime warning: GamesFeature.swift:70 Caught error: SQLite error 1: no such table: sqlitedata_icloud_metadata - while executing SELECT "games"."id" AS "id", "games"."title" AS "title", count("players"."id") AS "playerCount", ifnull("sqlitedata_icloud_metadata"."isShared", ?) AS "isShared" FROM "games" LEFT JOIN "players" ON ("games"."id") = ("players"."gameID") LEFT JOIN "sqlitedata_icloud_metadata" ON ("games"."id", ?) = ("sqlitedata_icloud_metadata"."recordPrimaryKey", "sqlitedata_icloud_metadata"."recordType") GROUP BY "games"."id" ORDER BY count("players"."id") DESC

24:35

This warning is happening because we don’t have access to the sync metadata table by default. We have to do a little bit of extra work to connect to the sync metadata. This is an example of one of those “programmer” errors we mentioned many times in this tour, and is definitely not appropriate to display to our user.

24:48

To fix this we will go over to the Schema.swift file, and update the database connection to attach the metadatabase: configuration.prepareDatabase { db in try db.attachMetadatabase() … }

25:14

Now when we run the app we will correctly see that the shared games have an icon next to them, whereas the private ones do not. Conclusion

25:37

We have now seen that we have direct access to all of the iCloud sync metadata associated with our records from within SQLite. There is even more you can do with this sync metadata, such as separate the list of games into two lists, the private ones versus the shared ones. We could also query for the participants and owner in each share so that we could display that information in the root list of games. And really the sky is the limit to what is possible. Brandon

26:02

And this now concludes our 4-part tour of SQLiteData. We hope you see that it does provide a lightweight and powerful alternative to SwiftData:

26:12

It allows us to embrace values types over reference types. Stephen

26:16

It gives us direct access to SQLite, which is incredibly powerful. Brandon

26:20

It provides type-safety and schema-safety to models and queries so that we don’t write code that compiles just fine by crashes at runtime. Stephen

26:29

It works in all varieties of apps, including SwiftUI, UIKit, AppKit, and maybe in the future even Windows, Linux and Wasm. Brandon

26:39

It handles assets beautifully. Stephen

26:43

And it supports iCloud synchronization and sharing records with other iCloud users.

26:48

And honestly, there’s still quite a bit more power in the library to explore, but we wanted to keep things simple for the tour. Brandon

26:56

And also if you are excited by those “Point-Free way” skill documents we demo’d in this series, then rest assured we will have more information to share about that soon. We are hard at work perfecting them, and can’t wait for people to get a hold of them to better make use of our libraries and best practices.

27:15

Until 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 0350-sqlite-data-tour-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 .