Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
45 changes: 32 additions & 13 deletions Examples/CloudKitDemo/CountersListFeature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,27 @@ import SQLiteData
import SwiftUI

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 @@ -24,10 +36,14 @@ struct CountersListView: View {
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("Add") {
withErrorReporting {
try database.write { db in
try Counter.insert { Counter.Draft() }
Task {
withErrorReporting {
try database.write { db in
try Counter.insert {
Counter.Draft()
}
.execute(db)
}
}
}
}
Expand All @@ -39,7 +55,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 @@ -48,15 +64,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 @@ -78,7 +97,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 @@ -87,7 +106,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 @@ -98,7 +117,7 @@ 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)
Expand Down

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

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,7 +253,74 @@
///
/// 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()
}
}
}
}
.navigationTitle("Share")
.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
Expand Down
7 changes: 1 addition & 6 deletions Sources/SQLiteData/CloudKit/Internal/Metadatabase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,7 @@
throw InMemoryDatabase()
}

let metadatabase: any DatabaseWriter =
if url.isInMemory {
try DatabaseQueue(path: url.absoluteString)
} else {
try DatabasePool(path: url.path(percentEncoded: false))
}
let metadatabase = try DatabasePool(path: url.path(percentEncoded: false))
try migrate(metadatabase: metadatabase)
return metadatabase
}
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
17 changes: 14 additions & 3 deletions Sources/SQLiteData/CloudKit/Internal/MockSyncEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
}

package func fetchChanges(_ options: CKSyncEngine.FetchChangesOptions) async throws {
let records: [CKRecord]
let modifications: [CKRecord]
let zoneIDs: [CKRecordZone.ID]
switch options.scope {
case .all:
Expand All @@ -46,13 +46,24 @@
@unknown default:
fatalError()
}
records = zoneIDs.reduce(into: [CKRecord]()) { accum, zoneID in
modifications = zoneIDs.reduce(into: [CKRecord]()) { accum, zoneID in
accum += database.storage.withValue {
($0[zoneID]?.records.values).map { Array($0) } ?? []
}
}
await parentSyncEngine.handleEvent(
.fetchedRecordZoneChanges(modifications: records, deletions: []),
.fetchedRecordZoneChanges(
modifications: modifications,
deletions: database.deletedRecords.withValue {
let records = $0.filter { recordID, _ in
zoneIDs.contains(recordID.zoneID)
}
$0.removeAll { lhs, _ in
records.contains { rhs, _ in lhs == rhs }
}
return records
}
),
syncEngine: self
)
}
Expand Down
39 changes: 35 additions & 4 deletions Sources/SQLiteData/CloudKit/SyncEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
private let notificationsObserver = LockIsolated<(any NSObjectProtocol)?>(nil)
private let activityCounts = LockIsolated(ActivityCounts())
private let startTask = LockIsolated<Task<Void, Never>?>(nil)
#if canImport(DeveloperToolsSupport)
private let previewTimerTask = LockIsolated<Task<Void, Never>?>(nil)
#endif

/// The error message used when a write occurs to a record for which the current user does not
/// have permission.
Expand Down Expand Up @@ -420,6 +423,12 @@
/// You must start the sync engine again using ``start()`` to synchronize the changes.
public func stop() {
guard isRunning else { return }
#if canImport(DeveloperToolsSupport)
previewTimerTask.withValue {
$0?.cancel()
$0 = nil
}
#endif
observationRegistrar.withMutation(of: self, keyPath: \.isRunning) {
syncEngines.withValue {
$0 = SyncEngines()
Expand Down Expand Up @@ -494,6 +503,23 @@
}
)

#if canImport(DeveloperToolsSupport)
@Dependency(\.context) var context
if context == .preview {
previewTimerTask.withValue {
$0?.cancel()
$0 = Task { [weak self] in
await withErrorReporting {
while true {
guard let self else { break }
try await Task.sleep(for: .seconds(1))
try await self.syncChanges()
}
}
}
}
}
#endif
let startTask = Task<Void, Never> {
await withErrorReporting(.sqliteDataCloudKitFailure) {
guard try await container.accountStatus() == .available
Expand All @@ -512,7 +538,10 @@
try await cacheUserTables(recordTypes: currentRecordTypes)
}
}
self.startTask.withValue { $0 = startTask }
self.startTask.withValue {
$0?.cancel()
$0 = startTask
}
return startTask
}

Expand Down Expand Up @@ -1502,7 +1531,8 @@
}
var unsyncedRecords: [CKRecord] = []
for start in stride(from: 0, to: orderedUnsyncedRecordIDs.count, by: batchSize) {
let recordIDsBatch = orderedUnsyncedRecordIDs
let recordIDsBatch =
orderedUnsyncedRecordIDs
.dropFirst(start)
.prefix(batchSize)
let results = try await syncEngine.database.records(for: Array(recordIDsBatch))
Expand Down Expand Up @@ -1586,7 +1616,7 @@
return false
case (_, nil):
return true
case let (.some(lhs), .some(rhs)):
case (.some(let lhs), .some(let rhs)):
let lhsIndex = tablesByOrder[lhs] ?? (rootFirst ? .max : .min)
let rhsIndex = tablesByOrder[rhs] ?? (rootFirst ? .max : .min)
guard lhsIndex != rhsIndex
Expand Down Expand Up @@ -2328,7 +2358,8 @@
tablesByName: [String: any SynchronizableTable]
) throws -> [String: Int] {
let tableDependencies = try userDatabase.read { db in
var dependencies: OrderedDictionary<HashableSynchronizedTable, [any SynchronizableTable]> = [:]
var dependencies: OrderedDictionary<HashableSynchronizedTable, [any SynchronizableTable]> =
[:]
for table in tables {
func open<T>(_: some SynchronizableTable<T>) throws -> [String] {
try PragmaForeignKeyList<T>
Expand Down
Loading
Loading