|
35 | 35 | let dataManager = Dependency(\.dataManager) |
36 | 36 | private let observationRegistrar = ObservationRegistrar() |
37 | 37 | private let notificationsObserver = LockIsolated<(any NSObjectProtocol)?>(nil) |
| 38 | + private let activityCounts = LockIsolated(ActivityCounts()) |
38 | 39 |
|
39 | 40 | /// The error message used when a write occurs to a record for which the current user |
40 | 41 | /// does not have permission. |
|
357 | 358 | public func start() async throws { |
358 | 359 | try await start().value |
359 | 360 | } |
| 361 | + |
| 362 | + /// Determines if the sync engine is currently sending local changes to the CloudKit server. |
| 363 | + /// |
| 364 | + /// It is an observable value, which means if it is accessed in a SwiftUI view, or some other |
| 365 | + /// observable context, then the view will automatically re-render when the value changes. As |
| 366 | + /// such, it can be useful for displaying a progress view to indicate that work is currently |
| 367 | + /// being done to synchronize changes. |
| 368 | + public var isSendingChanges: Bool { |
| 369 | + sendingChangesCount > 0 |
| 370 | + } |
| 371 | + |
| 372 | + /// Determines if the sync engine is currently processing changes being sent to the device |
| 373 | + /// from CloudKit. |
| 374 | + /// |
| 375 | + /// It is an observable value, which means if it is accessed in a SwiftUI view, or some other |
| 376 | + /// observable context, then the view will automatically re-render when the value changes. As |
| 377 | + /// such, it can be useful for displaying a progress view to indicate that work is currently |
| 378 | + /// being done to synchronize changes. |
| 379 | + public var isFetchingChanges: Bool { |
| 380 | + fetchingChangesCount > 0 |
| 381 | + } |
| 382 | + |
| 383 | + /// Determines if the sync engine is currently sending or receiving changes from CloudKit. |
| 384 | + /// |
| 385 | + /// This value is true if either of ``isSendingChanges`` or ``isFetchingChanges`` is true. |
| 386 | + /// It is an observable value, which means if it is accessed in a SwiftUI view, or some other |
| 387 | + /// observable context, then the view will automatically re-render when the value changes. As |
| 388 | + /// such, it can be useful for displaying a progress view to indicate that work is currently |
| 389 | + /// being done to synchronize changes. |
| 390 | + public var isSynchronizing: Bool { |
| 391 | + isSendingChanges || isFetchingChanges |
| 392 | + } |
360 | 393 |
|
361 | 394 | /// Stops the sync engine if it is running. |
362 | 395 | /// |
|
399 | 432 | try SQLiteSchema |
400 | 433 | .where { |
401 | 434 | $0.type.eq(#bind(.table)) |
402 | | - && $0.tableName.in(tables.map { $0.base.tableName }) |
| 435 | + && $0.tableName.in(tables.map { $0.base.tableName }) |
403 | 436 | } |
404 | 437 | .fetchAll(db) |
405 | 438 | return try namesAndSchemas.compactMap { schema -> RecordType? in |
|
737 | 770 | public static func isSynchronizingChanges() -> some QueryExpression<Bool> { |
738 | 771 | $syncEngineIsSynchronizingChanges() |
739 | 772 | } |
| 773 | + |
| 774 | + private var sendingChangesCount: Int { |
| 775 | + get { |
| 776 | + observationRegistrar.access(self, keyPath: \.isSendingChanges) |
| 777 | + return activityCounts.withValue(\.sendingChangesCount) |
| 778 | + } |
| 779 | + set { |
| 780 | + observationRegistrar.withMutation(of: self, keyPath: \.isSendingChanges) { |
| 781 | + activityCounts.withValue { $0.sendingChangesCount = newValue } |
| 782 | + } |
| 783 | + } |
| 784 | + } |
| 785 | + private var fetchingChangesCount: Int { |
| 786 | + get { |
| 787 | + observationRegistrar.access(self, keyPath: \.isFetchingChanges) |
| 788 | + return activityCounts.withValue(\.fetchingChangesCount) |
| 789 | + } |
| 790 | + set { |
| 791 | + observationRegistrar.withMutation(of: self, keyPath: \.isFetchingChanges) { |
| 792 | + activityCounts.withValue { $0.fetchingChangesCount = newValue } |
| 793 | + } |
| 794 | + } |
| 795 | + } |
740 | 796 | } |
741 | 797 |
|
742 | 798 | extension PrimaryKeyedTable { |
|
814 | 870 | failedRecordDeletes: failedRecordDeletes, |
815 | 871 | syncEngine: syncEngine |
816 | 872 | ) |
817 | | - case .willFetchRecordZoneChanges, .didFetchRecordZoneChanges, .willFetchChanges, |
818 | | - .didFetchChanges, .willSendChanges, .didSendChanges: |
819 | | - break |
| 873 | + |
| 874 | + case .willFetchRecordZoneChanges: |
| 875 | + fetchingChangesCount += 1 |
| 876 | + case .didFetchRecordZoneChanges: |
| 877 | + fetchingChangesCount -= 1 |
| 878 | + |
| 879 | + case .willFetchChanges: |
| 880 | + fetchingChangesCount += 1 |
| 881 | + case .didFetchChanges: |
| 882 | + fetchingChangesCount -= 1 |
| 883 | + |
| 884 | + case .willSendChanges: |
| 885 | + sendingChangesCount += 1 |
| 886 | + case .didSendChanges: |
| 887 | + sendingChangesCount -= 1 |
| 888 | + |
820 | 889 | @unknown default: |
821 | 890 | break |
822 | 891 | } |
|
2054 | 2123 | tablesByName: [String: any SynchronizableTable] |
2055 | 2124 | ) throws -> [String: Int] { |
2056 | 2125 | let tableDependencies = try userDatabase.read { db in |
2057 | | - var dependencies: |
2058 | | - [HashableSynchronizedTable: [any SynchronizableTable]] = [:] |
| 2126 | + var dependencies: [HashableSynchronizedTable: [any SynchronizableTable]] = [:] |
2059 | 2127 | for table in tables { |
2060 | 2128 | func open<T>(_: some SynchronizableTable<T>) throws -> [String] { |
2061 | 2129 | try PragmaForeignKeyList<T>.select(\.table) |
|
2172 | 2240 | _currentZoneID?.ownerName |
2173 | 2241 | } |
2174 | 2242 |
|
| 2243 | + private struct ActivityCounts { |
| 2244 | + var sendingChangesCount = 0 |
| 2245 | + var fetchingChangesCount = 0 |
| 2246 | + } |
| 2247 | + |
2175 | 2248 | #if DEBUG |
2176 | 2249 | @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) |
2177 | 2250 | private struct NextRecordZoneChangeBatchLoggingState { |
|
0 commit comments