CloudKitSyncMonitor
is a Swift package that listens to notifications sent out by NSPersistentCloudKitContainer
and translates them into published properties, providing your app with real-time sync state information.
This package addresses a critical issue where CloudKit (and consequently your app) may cease syncing without warning or user notification. CloudKitSyncMonitor
offers immediate detection of such scenarios, allowing you to promptly inform users and take appropriate action.
- ๐ก Monitors sync status by intercepting and interpreting
NSPersistentCloudKitContainer
notifications - ๐ง Intelligently assesses sync health by considering both network availability and iCloud account status
- ๐ Exposes a
SyncMonitor
class, conveniently accessible via theSyncMonitor.default
singleton
SyncMonitor
actively subscribes to notifications from key system components:
- ๐
NSPersistentCloudKitContainer
: For core sync event monitoring - โ๏ธ
CKContainer
: To track CloudKit-specific states - ๐
NWPathMonitor
: For network status updates
Important: SyncMonitor.default.startMonitoring()
as early as possible in your app's lifecycle, preferably in your app delegate or initial view.
SyncMonitor
provides sync information at two distinct levels of granularity:
The syncStateSummary
property offers a high-level enum summarizing the overall sync state. This is ideal for quick status checks and user-facing information.
SyncMonitor
tracks the states of NSPersistentCloudKitContainer
's three primary event types:
- Setup: Initialization of the sync environment
- Import: Incoming data from CloudKit to the local store
- Export: Outgoing data from the local store to CloudKit
To monitor these events, SyncMonitor
provides corresponding properties:
setupState
: Tracks the state of the setup eventimportState
: Monitors the state of the import eventexportState
: Follows the state of the export event
These properties provide comprehensive information about each sync phase, including convenience methods for extracting commonly needed details.
SyncMonitor
offers robust tools for identifying sync issues:
- ๐ด
hasSyncError
: A Boolean indicating the presence of any sync-related error - ๐ก
isNotSyncing
: Detects scenarios where sync should be operational but isn't functioning as expected
setupError
: Captures issues during the sync setup phaseimportError
: Identifies problems with data import from CloudKitexportError
: Highlights issues when exporting data to CloudKit
The isNotSyncing
property is particularly useful for detecting subtle sync issues:
- It indicates when setup has completed successfully, but no import event has started, and no errors have been reported
- This can reveal edge cases like OS-level password re-entry prompts, where CloudKit considers the account "available", but
NSPersistentCloudKitContainer
is unable to initiate sync - Like other properties, it factors in network availability and iCloud account status for accurate reporting
Timely and accurate error detection is crucial for maintaining data integrity and user trust:
- ๐ก๏ธ Prevents potential data loss by identifying sync failures before they lead to conflicts or data divergence
- โก Enables immediate detection and reporting of sync anomalies, often before users notice any issues
- ๐ Significantly enhances user experience by providing transparent, real-time sync status information
- ๐ Helps maintain app reliability and data consistency across devices
The setupState
, importState
, and exportState
properties offer comprehensive insights into the sync process:
- Current state of each event type (not started, in progress, succeeded, or failed)
- Precise start and end times for each sync event
- Detailed error information when applicable
Example usage for displaying detailed sync status:
fileprivate var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
return dateFormatter
}()
print("Setup state: \(stateText(for: SyncMonitor.default.setupState))")
print("Import state: \(stateText(for: SyncMonitor.default.importState))")
print("Export state: \(stateText(for: SyncMonitor.default.exportState))")
func stateText(for state: SyncMonitor.SyncState) -> String {
switch state {
case .notStarted:
return "Not started"
case .inProgress(started: let date):
return "In progress since \(dateFormatter.string(from: date))"
case let .succeeded(started: _, ended: endDate):
return "Succeeded at \(dateFormatter.string(from: endDate))"
case let .failed(started: _, ended: endDate, error: _):
return "Failed at \(dateFormatter.string(from: endDate))"
}
}
For more detailed information on all available properties and methods, please refer to the comprehensive SyncMonitor documentation.
private let syncMonitor = SyncMonitor.default
if syncMonitor.hasSyncError {
if let error = syncMonitor.setupError {
print("Unable to set up iCloud sync, changes won't be saved! \(error.localizedDescription)")
}
if let error = syncMonitor.importError {
print("Import is broken: \(error.localizedDescription)")
}
if let error = syncMonitor.exportError {
print("Export is broken - your changes aren't being saved! \(error.localizedDescription)")
}
} else if syncMonitor.isNotSyncing {
print("Sync should be working, but isn't. Look for a badge on Settings or other possible issues.")
}
import SwiftUI
import CloudKitSyncMonitor
struct SyncStatusView: View {
@StateObject private var syncMonitor = SyncMonitor.default
var body: some View {
if syncMonitor.syncStateSummary.isBroken {
Image(systemName: syncMonitor.syncStateSummary.symbolName)
.foregroundColor(syncMonitor.syncStateSummary.symbolColor)
}
}
}
import SwiftUI
import CloudKitSyncMonitor
struct SyncStatusView: View {
@StateObject private var syncMonitor = SyncMonitor.default
var body: some View {
Image(systemName: syncMonitor.syncStateSummary.symbolName)
.foregroundColor(syncMonitor.syncStateSummary.symbolColor)
}
}
if syncMonitor.syncStateSummary.isBroken || syncMonitor.syncStateSummary.isInProgress {
Image(systemName: syncMonitor.syncStateSummary.symbolName)
.foregroundColor(syncMonitor.syncStateSummary.symbolColor)
}
if case .accountNotAvailable = syncMonitor.syncStateSummary {
Text("Hey, log into your iCloud account if you want to sync")
}
Add the following to your Package.swift
:
dependencies: [
.package(url: "https://github.com/ggruen/CloudKitSyncMonitor.git", from: "3.0.0"),
],
targets: [
.target(
name: "MyApp", // Where "MyApp" is the name of your app
dependencies: ["CloudKitSyncMonitor"]),
]
- Select File ยป Swift Packages ยป Add Package Dependency...
- Enter the repository URL:
https://github.com/ggruen/CloudKitSyncMonitor.git
- Choose "Up to next major version" with
3.0.0
as the minimum version.
- ๐ด Fork repository
- ๐ฅ Check out on your development system
- ๐ Drag the folder this README is in (CloudKitSyncMonitor) into your Xcode project or workspace. This will make Xcode choose the local version over the version in the package manager.
- ๐ง If you haven't added it via File > Swift Packages already, go into your project > General tab > Frameworks, Libraries and Embedded Content, and click the + button to add it. You may need to quit and re-start Xcode to make the new package appear in the list so you can select it.
- ๐๏ธ Make your changes, commit and push to Github
- ๐ Submit pull request
To go back to using the github version, just remove CloudKitSyncMonitor (click on it, hit the delete key, choose to remove reference) from the side bar - Xcode should fall back to the version you added using the Installation instructions above. If you haven't installed it as a package dependency yet, then just delete it from the side bar and then add it as a package dependency using the Installation instructions above.
You can also submit issues if you find bugs, have suggestions or questions, etc. ๐๐กโ