Add an iCloud diagnostics screen
A new Settings > Diagnostics screen reports the ubiquity container and account status, per-document download and eviction state, network readiness, and a count of documents skipped by the schema-version forward gate — surfaced to help debug why a file isn't syncing. Claude-Session: https://claude.ai/code/session_01HJDQQDA9QdP8zByg43H5v3
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// NetworkReadiness.swift
|
||||
// Reference code — copy into your app, adjust the type/file names to taste.
|
||||
//
|
||||
// Adapted from the IndieDiag package (`NetworkDiagnostic.swift`,
|
||||
// commit bc6f475). Trimmed to the NWPathMonitor-based status/quality
|
||||
// estimate only — dropped the original's `testCloudConnectivity()` HTTP
|
||||
// probe to icloud.com as extra surface a documents-only app doesn't need;
|
||||
// `NWPathMonitor`'s path status is enough to decide "is this a reasonable
|
||||
// moment to sync." Combine publishers replaced with a plain `async`
|
||||
// one-shot read plus an `AsyncStream` for the rare screen that wants live
|
||||
// updates while visible.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
/// Network connection type.
|
||||
public enum ConnectionType: String, Sendable {
|
||||
case wifi = "Wi-Fi"
|
||||
case cellular = "Cellular"
|
||||
case ethernet = "Ethernet"
|
||||
case unknown = "Unknown"
|
||||
case none = "No Connection"
|
||||
}
|
||||
|
||||
/// Network quality estimate for syncing.
|
||||
public enum NetworkQuality: String, Sendable {
|
||||
case excellent = "Excellent"
|
||||
case good = "Good"
|
||||
case fair = "Fair"
|
||||
case poor = "Poor"
|
||||
case none = "No Connection"
|
||||
|
||||
/// Suitable for large file uploads (e.g. media attachments).
|
||||
public var canSyncLargeFiles: Bool { self == .excellent || self == .good }
|
||||
|
||||
/// Suitable for basic sync operations (small JSON documents).
|
||||
public var canSync: Bool { self != .none }
|
||||
}
|
||||
|
||||
/// Network connectivity status relevant to iCloud sync.
|
||||
public struct NetworkStatus: Sendable {
|
||||
public let isConnected: Bool
|
||||
public let connectionType: ConnectionType
|
||||
public let isExpensive: Bool
|
||||
public let isConstrained: Bool
|
||||
public let supportsCloudSync: Bool
|
||||
public let lastChecked: Date
|
||||
|
||||
public init(
|
||||
isConnected: Bool,
|
||||
connectionType: ConnectionType,
|
||||
isExpensive: Bool = false,
|
||||
isConstrained: Bool = false,
|
||||
supportsCloudSync: Bool = true,
|
||||
lastChecked: Date = Date()
|
||||
) {
|
||||
self.isConnected = isConnected
|
||||
self.connectionType = connectionType
|
||||
self.isExpensive = isExpensive
|
||||
self.isConstrained = isConstrained
|
||||
self.supportsCloudSync = supportsCloudSync
|
||||
self.lastChecked = lastChecked
|
||||
}
|
||||
|
||||
public var quality: NetworkQuality {
|
||||
guard isConnected else { return .none }
|
||||
if isConstrained { return .poor }
|
||||
switch connectionType {
|
||||
case .wifi, .ethernet: return .excellent
|
||||
case .cellular: return isExpensive ? .fair : .good
|
||||
case .unknown, .none: return .poor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `NWPathMonitor`-based network readiness check. Two ways to use it:
|
||||
///
|
||||
/// - `currentStatus()` — a one-shot read for a diagnostics screen's initial
|
||||
/// state, or before starting a sync operation.
|
||||
/// - `statusUpdates()` — an `AsyncStream` for as long as the screen is
|
||||
/// visible and iterating it. Stop iterating (or cancel the owning `Task`)
|
||||
/// when the screen disappears; don't leave a monitor running for the
|
||||
/// app's whole lifetime just for a diagnostics view.
|
||||
public actor NetworkReadiness {
|
||||
private var monitor: NWPathMonitor?
|
||||
|
||||
public init() {}
|
||||
|
||||
/// Single point-in-time read: starts a path monitor, takes the first
|
||||
/// update, and cancels. Cheap enough to call every time a diagnostics
|
||||
/// screen appears — don't cache this across app launches or poll it on
|
||||
/// a timer (a real path change will call any *ongoing* monitor's
|
||||
/// handler immediately; polling adds nothing but battery drain).
|
||||
public func currentStatus() async -> NetworkStatus {
|
||||
let monitor = NWPathMonitor()
|
||||
return await withCheckedContinuation { continuation in
|
||||
monitor.pathUpdateHandler = { path in
|
||||
continuation.resume(returning: Self.buildStatus(from: path))
|
||||
monitor.cancel()
|
||||
}
|
||||
monitor.start(queue: DispatchQueue(label: "indie-diag.network.oneshot"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Ongoing updates for as long as the returned stream is iterated.
|
||||
/// Only one monitor runs at a time per `NetworkReadiness` instance —
|
||||
/// starting a new stream cancels any previous one.
|
||||
public func statusUpdates() -> AsyncStream<NetworkStatus> {
|
||||
stop()
|
||||
let monitor = NWPathMonitor()
|
||||
self.monitor = monitor
|
||||
|
||||
return AsyncStream { continuation in
|
||||
monitor.pathUpdateHandler = { path in
|
||||
continuation.yield(Self.buildStatus(from: path))
|
||||
}
|
||||
continuation.onTermination = { _ in
|
||||
monitor.cancel()
|
||||
}
|
||||
monitor.start(queue: DispatchQueue(label: "indie-diag.network.monitor"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancels any monitor started by `statusUpdates()`. Call this when the
|
||||
/// diagnostics screen disappears if you're not relying on the stream's
|
||||
/// consumer dropping out to trigger `onTermination`.
|
||||
public func stop() {
|
||||
monitor?.cancel()
|
||||
monitor = nil
|
||||
}
|
||||
|
||||
private nonisolated static func buildStatus(from path: NWPath) -> NetworkStatus {
|
||||
let isConnected = path.status == .satisfied
|
||||
|
||||
let connectionType: ConnectionType
|
||||
if path.usesInterfaceType(.wifi) {
|
||||
connectionType = .wifi
|
||||
} else if path.usesInterfaceType(.cellular) {
|
||||
connectionType = .cellular
|
||||
} else if path.usesInterfaceType(.wiredEthernet) {
|
||||
connectionType = .ethernet
|
||||
} else if isConnected {
|
||||
connectionType = .unknown
|
||||
} else {
|
||||
connectionType = .none
|
||||
}
|
||||
|
||||
let isExpensive = path.isExpensive
|
||||
let isConstrained = path.isConstrained
|
||||
|
||||
// iCloud sync works best on Wi-Fi/Ethernet, or unconstrained cellular.
|
||||
let supportsCloudSync = isConnected && (!isConstrained || connectionType == .wifi || connectionType == .ethernet)
|
||||
|
||||
return NetworkStatus(
|
||||
isConnected: isConnected,
|
||||
connectionType: connectionType,
|
||||
isExpensive: isExpensive,
|
||||
isConstrained: isConstrained,
|
||||
supportsCloudSync: supportsCloudSync
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user