Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
105a55f
wip
stephencelis Jan 9, 2026
3969174
wip
stephencelis Jan 9, 2026
03a6ff3
wip
stephencelis Jan 9, 2026
bef7064
wip
stephencelis Jan 9, 2026
a6cc565
wip
stephencelis Jan 9, 2026
c8ce17d
Better support for sharing in previews.
mbrandonw Jan 9, 2026
c847dfd
wip
mbrandonw Jan 9, 2026
2b2deaf
wip
mbrandonw Jan 9, 2026
f3c7375
wip
mbrandonw Jan 9, 2026
3d15e5e
clean up
mbrandonw Jan 9, 2026
3b686f1
wip
mbrandonw Jan 9, 2026
7ae4728
clean up
mbrandonw Jan 9, 2026
0066e5d
Trying out database pool for previews.
mbrandonw Jan 9, 2026
e673c67
wip
mbrandonw Jan 10, 2026
82f0724
Merge remote-tracking branch 'origin/main' into better-preview-sharing
mbrandonw Jan 10, 2026
c04f60f
wip
mbrandonw Jan 10, 2026
253d3ae
clean up
mbrandonw Jan 10, 2026
044368f
clean up
mbrandonw Jan 10, 2026
1165fb8
reformat
mbrandonw Jan 10, 2026
9d49470
cancel timer when stopping sync engine
mbrandonw Jan 10, 2026
8ed34cd
clean up
mbrandonw Jan 10, 2026
8f556ee
wip
mbrandonw Jan 10, 2026
ca01d18
wip
mbrandonw Jan 10, 2026
eae64d1
wip
mbrandonw Jan 11, 2026
ba92711
clean up
mbrandonw Jan 11, 2026
22b98c7
wip
mbrandonw Jan 11, 2026
5df94bb
fixes
mbrandonw Jan 11, 2026
090dd74
fix
mbrandonw Jan 11, 2026
43727d5
wip
mbrandonw Jan 11, 2026
6d5e42b
wip
mbrandonw Jan 11, 2026
48b8f42
Revert "fix"
mbrandonw Jan 11, 2026
89941f7
Revert "wip"
mbrandonw Jan 11, 2026
84a476d
Revert "wip"
mbrandonw Jan 11, 2026
87a7876
modernize previews
mbrandonw Jan 12, 2026
4478fbf
wip
stephencelis Jan 12, 2026
226b2ca
lock isolated
mbrandonw Jan 12, 2026
175d3ca
wip
mbrandonw Jan 12, 2026
8cacfbe
wiup
mbrandonw Jan 12, 2026
2a81955
clean up
mbrandonw Jan 13, 2026
640a8fa
Merge remote-tracking branch 'origin/main' into better-preview-sharing
mbrandonw Jan 13, 2026
9a567a4
Fix merge conflicts.
mbrandonw Jan 13, 2026
22d4400
Merge remote-tracking branch 'origin/main' into better-preview-sharing
stephencelis Jan 13, 2026
5616370
wip
stephencelis Jan 13, 2026
9a6a61e
format
stephencelis Jan 13, 2026
8be0439
wip
mbrandonw Jan 13, 2026
93d5124
wip
stephencelis Jan 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Examples/CloudKitDemo/CloudKitDemoApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import SwiftUI
@main
struct CloudKitDemoApp: App {
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
@Dependency(\.context) var context

init() {
try! prepareDependencies {
try $0.bootstrapDatabase()
if context == .live {
try! prepareDependencies {
try $0.bootstrapDatabase()
}
}
}

Expand Down
55 changes: 40 additions & 15 deletions Examples/CloudKitDemo/CountersListFeature.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,29 @@
import CloudKit
import SQLiteData
import SwiftUI
import SwiftUINavigation

struct CountersListView: View {
@FetchAll var counters: [Counter]
@FetchAll(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the CloudKitDemo to show an icon next to counters that are shared. We can now explore that functionality in previews thanks to the changes in this PR.

Counter
.leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($1.id) }
.select {
Row.Columns(counter: $0, isShared: $1.isShared.ifnull(false))
}
) var rows
@Dependency(\.defaultDatabase) var database
@Dependency(\.defaultSyncEngine) var syncEngine

@Selection struct Row {
let counter: Counter
let isShared: Bool
}

var body: some View {
List {
if !counters.isEmpty {
if !rows.isEmpty {
Section {
ForEach(counters) { counter in
CounterRow(counter: counter)
ForEach(rows, id: \.counter.id) { row in
CounterRow(row: row)
.buttonStyle(.borderless)
}
.onDelete { indexSet in
Expand All @@ -25,10 +36,12 @@ struct CountersListView: View {
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Add") {
withErrorReporting {
try database.write { db in
try Counter.insert { Counter.Draft() }
.execute(db)
Task {
withErrorReporting {
try database.write { db in
try Counter.insert { Counter.Draft() }
.execute(db)
}
}
}
}
Expand All @@ -40,7 +53,7 @@ struct CountersListView: View {
withErrorReporting {
try database.write { db in
for index in indexSet {
try Counter.find(counters[index].id).delete()
try Counter.find(rows[index].counter.id).delete()
.execute(db)
}
}
Expand All @@ -49,15 +62,18 @@ struct CountersListView: View {
}

struct CounterRow: View {
let counter: Counter
let row: CountersListView.Row
@State var sharedRecord: SharedRecord?
@Dependency(\.defaultDatabase) var database
@Dependency(\.defaultSyncEngine) var syncEngine

var body: some View {
VStack {
HStack {
Text("\(counter.count)")
if row.isShared {
Image(systemName: "network")
}
Text("\(row.counter.count)")
Button("-") {
decrementButtonTapped()
}
Expand All @@ -79,7 +95,7 @@ struct CounterRow: View {

func shareButtonTapped() {
Task {
sharedRecord = try await syncEngine.share(record: counter) { share in
sharedRecord = try await syncEngine.share(record: row.counter) { share in
share[CKShare.SystemFieldKey.title] = "Join my counter!"
}
}
Expand All @@ -88,7 +104,7 @@ struct CounterRow: View {
func decrementButtonTapped() {
withErrorReporting {
try database.write { db in
try Counter.find(counter.id).update {
try Counter.find(row.counter.id).update {
$0.count -= 1
}
.execute(db)
Expand All @@ -99,11 +115,20 @@ struct CounterRow: View {
func incrementButtonTapped() {
withErrorReporting {
try database.write { db in
try Counter.find(counter.id).update {
try Counter.find(row.counter.id).update {
$0.count += 1
}
.execute(db)
}
}
}
}

#Preview {
let _ = try! prepareDependencies {
try $0.bootstrapDatabase()
}
NavigationStack {
CountersListView()
}
}
7 changes: 5 additions & 2 deletions Examples/CloudKitDemo/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ nonisolated struct Counter: Identifiable {

extension DependencyValues {
mutating func bootstrapDatabase() throws {
@Dependency(\.context) var context
let database = try SQLiteData.defaultDatabase()
var configuration = Configuration()
configuration.prepareDatabase { db in
try db.attachMetadatabase()
}
let database = try SQLiteData.defaultDatabase(configuration: configuration)
logger.debug(
"""
App database
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Examples/SyncUps/Schema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ extension Int {

extension DependencyValues {
mutating func bootstrapDatabase() throws {
@Dependency(\.context) var context
let database = try SQLiteData.defaultDatabase()
logger.debug(
"""
Expand Down
73 changes: 72 additions & 1 deletion Sources/SQLiteData/CloudKit/CloudKitSharing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@
return
}

try await unshare(share: share)
}

func unshare(share: CKShare) async throws {
let result = try await syncEngines.private?.database.modifyRecords(
saving: [],
deleting: [share.recordID]
Expand All @@ -249,12 +253,79 @@
///
/// See <doc:CloudKitSharing#Creating-CKShare-records> for more info.
@available(iOS 17, macOS 14, tvOS 17, *)
public struct CloudSharingView: UIViewControllerRepresentable {
public struct CloudSharingView: View {
let sharedRecord: SharedRecord
let availablePermissions: UICloudSharingController.PermissionOptions
let didFinish: (Result<Void, Error>) -> Void
let didStopSharing: () -> Void
let syncEngine: SyncEngine
@Dependency(\.context) var context
@Environment(\.dismiss) var dismiss
public init(
sharedRecord: SharedRecord,
availablePermissions: UICloudSharingController.PermissionOptions = [],
didFinish: @escaping (Result<Void, Error>) -> Void = { _ in },
didStopSharing: @escaping () -> Void = {},
syncEngine: SyncEngine = {
@Dependency(\.defaultSyncEngine) var defaultSyncEngine
return defaultSyncEngine
}()
) {
self.sharedRecord = sharedRecord
self.didFinish = didFinish
self.didStopSharing = didStopSharing
self.availablePermissions = availablePermissions
self.syncEngine = syncEngine
}
public var body: some View {
if context == .live {
CloudSharingViewRepresentable(
sharedRecord: sharedRecord,
availablePermissions: availablePermissions,
didFinish: didFinish,
didStopSharing: didStopSharing,
syncEngine: syncEngine
)
} else {
Form {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We now emulate a cloud sharing view in previews!

Section {
if let title = sharedRecord.share[CKShare.SystemFieldKey.title] as? String {
Text(title)
}
if
let imageData = sharedRecord.share[CKShare.SystemFieldKey.thumbnailImageData] as? Data,
let image = UIImage(data: imageData)
{
Image(uiImage: image)
}
}
Section {
Button("Stop sharing", role: .destructive) {
Task {
try await syncEngine.unshare(share: sharedRecord.share)
try await syncEngine.fetchChanges()
dismiss()
}
}
}
}
.task {
await withErrorReporting {
try await syncEngine.fetchChanges()
}
}
}
}
}

@available(iOS 17, macOS 14, tvOS 17, *)
private struct CloudSharingViewRepresentable: UIViewControllerRepresentable {
let sharedRecord: SharedRecord
let availablePermissions: UICloudSharingController.PermissionOptions
let didFinish: (Result<Void, Error>) -> Void
let didStopSharing: () -> Void
let syncEngine: SyncEngine
@Dependency(\.context) var context
public init(
sharedRecord: SharedRecord,
availablePermissions: UICloudSharingController.PermissionOptions = [],
Expand Down
6 changes: 6 additions & 0 deletions Sources/SQLiteData/CloudKit/Internal/MockCloudDatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package let databaseScope: CKDatabase.Scope
let _container = IsolatedWeakVar<MockCloudContainer>()
let dataManager = Dependency(\.dataManager)
let deletedRecords = LockIsolated<[(CKRecord.ID, CKRecord.RecordType)]>([])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We keep track of records deleted from the mock cloud database so that when fetching changes we can fetch deletions.


struct AssetID: Hashable {
let recordID: CKRecord.ID
Expand Down Expand Up @@ -261,6 +262,9 @@
let recordToDelete = storage[recordIDToDelete.zoneID]?.records[recordIDToDelete]
storage[recordIDToDelete.zoneID]?.records[recordIDToDelete] = nil
deleteResults[recordIDToDelete] = .success(())
if let recordType = recordToDelete?.recordType {
deletedRecords.withValue { $0.append((recordIDToDelete, recordType)) }
}

// NB: If deleting a share that the current user owns, delete the shared records and all
// associated records.
Expand All @@ -277,6 +281,8 @@
continue
}
storage[recordIDToDelete.zoneID]?.records[recordToDelete.recordID] = nil
deleteResults[recordToDelete.recordID] = .success(())
deletedRecords.withValue { $0.append((recordIDToDelete, recordToDelete.recordType)) }
deleteRecords(referencing: recordToDelete.recordID)
}
}
Expand Down
Loading