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