/CloudKitSyncMonitor

Monitor current state of NSPersistentCloudKitContainer sync

Primary LanguageSwiftMIT LicenseMIT

CloudKitSyncMonitor

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.

Swift Version License Platform PRs Welcome

Features and Behavior ๐ŸŒŸ

Core Functionality ๐Ÿ› ๏ธ

  • ๐Ÿ“ก 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 the SyncMonitor.default singleton

Notification Subscriptions ๐Ÿ“ฌ

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: โš ๏ธ To ensure accurate and timely state information, call SyncMonitor.default.startMonitoring() as early as possible in your app's lifecycle, preferably in your app delegate or initial view.

Information Levels ๐Ÿ“Š

SyncMonitor provides sync information at two distinct levels of granularity:

Top Level

The syncStateSummary property offers a high-level enum summarizing the overall sync state. This is ideal for quick status checks and user-facing information.

Detailed Level

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 event
  • importState: Monitors the state of the import event
  • exportState: Follows the state of the export event

These properties provide comprehensive information about each sync phase, including convenience methods for extracting commonly needed details.

Problem Detection ๐Ÿšจ

SyncMonitor offers robust tools for identifying sync issues:

General Detection

  • ๐Ÿ”ด 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

Specific Error Information

  • setupError: Captures issues during the sync setup phase
  • importError: Identifies problems with data import from CloudKit
  • exportError: Highlights issues when exporting data to CloudKit

Special Properties ๐Ÿ”‘

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

Importance of Error Detection โš ๏ธ

Timely and accurate error detection is crucial for maintaining data integrity and user trust:

  1. ๐Ÿ›ก๏ธ Prevents potential data loss by identifying sync failures before they lead to conflicts or data divergence
  2. โšก Enables immediate detection and reporting of sync anomalies, often before users notice any issues
  3. ๐Ÿ˜Š Significantly enhances user experience by providing transparent, real-time sync status information
  4. ๐Ÿ† Helps maintain app reliability and data consistency across devices

Detailed Sync Information ๐Ÿ“‹

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.

Usage Examples ๐Ÿš€

Handle Errors

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.")
}

Display Error Status

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)
         }
    }
}

Display Current Sync State

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)
    }
}

Conditional Display

if syncMonitor.syncStateSummary.isBroken || syncMonitor.syncStateSummary.isInProgress {
    Image(systemName: syncMonitor.syncStateSummary.symbolName)
        .foregroundColor(syncMonitor.syncStateSummary.symbolColor)
}

Check for Specific States

if case .accountNotAvailable = syncMonitor.syncStateSummary {
    Text("Hey, log into your iCloud account if you want to sync")
}

Installation ๐Ÿ“ฆ

Swift Package Manager

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"]),
]

Xcode

  1. Select File ยป Swift Packages ยป Add Package Dependency...
  2. Enter the repository URL: https://github.com/ggruen/CloudKitSyncMonitor.git
  3. Choose "Up to next major version" with 3.0.0 as the minimum version.

Development ๐Ÿ› ๏ธ

  • ๐Ÿด 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. ๐Ÿ›๐Ÿ’กโ“