// // ContainerStatus.swift // Reference code — copy into your app, adjust the type/file names to taste. // // Adapted from the IndieDiag package (`iCloudContainerDiagnostic.swift`, // commit bc6f475). The original resolved account status via // `CKContainer.accountStatus`, which pulls in CloudKit for a fact // `FileManager` already knows: `ubiquityIdentityToken` is non-nil exactly // when an iCloud account is signed in, checked synchronously with no // network round trip and no CloudKit entitlement required. That swap is // the whole reason this file has no `import CloudKit`. // // Trade-off: `ubiquityIdentityToken` only distinguishes "signed in" from // "not signed in." It cannot tell you *why* an account is unavailable // (restricted, temporarily unavailable, etc.) the way CloudKit's richer // `CKAccountStatus` can — that granularity isn't worth reintroducing // CloudKit for a documents-only app. If you need it, it means you've // reintroduced CloudKit somewhere else too. // import Foundation /// Availability and configuration of the app's iCloud ubiquity container. public struct ICloudContainerStatus: Sendable { public let isAvailable: Bool public let containerURL: URL? public let containerIdentifier: String? public let hasSignedInAccount: Bool public let lastChecked: Date public init( isAvailable: Bool, containerURL: URL?, containerIdentifier: String?, hasSignedInAccount: Bool, lastChecked: Date = Date() ) { self.isAvailable = isAvailable self.containerURL = containerURL self.containerIdentifier = containerIdentifier self.hasSignedInAccount = hasSignedInAccount self.lastChecked = lastChecked } } /// Point-in-time checks for the iCloud ubiquity container: is an account /// signed in, and can the container's URL be resolved. An `actor` because /// `url(forUbiquityContainerIdentifier:)` is a blocking FileManager call /// that can stall indefinitely when the account is in a bad state (password /// reverification pending is the classic case per the icloud-sync-engine /// skill) — actor isolation keeps that stall off whichever thread awaits it, /// in practice the main thread. /// /// This does point-in-time checks, not continuous monitoring. Call it when /// a diagnostics screen appears or the user taps "Refresh" — see /// SKILL.md's anti-patterns for why this shouldn't run on a timer. public actor ContainerDiagnostic { private let containerIdentifier: String? /// - Parameter containerIdentifier: iCloud container identifier. Pass /// `nil` to use the app's default (single) ubiquity container. public init(containerIdentifier: String? = nil) { self.containerIdentifier = containerIdentifier } /// Full status snapshot: signed-in check + container URL resolution. public func checkStatus() async -> ICloudContainerStatus { let hasAccount = FileManager.default.ubiquityIdentityToken != nil let containerURL = FileManager.default.url(forUbiquityContainerIdentifier: containerIdentifier) return ICloudContainerStatus( isAvailable: hasAccount && containerURL != nil, containerURL: containerURL, containerIdentifier: containerIdentifier, hasSignedInAccount: hasAccount ) } /// Convenience for a plain yes/no check. public func isAvailable() async -> Bool { await checkStatus().isAvailable } /// The `Documents/` subdirectory inside the container, creating it if /// needed. Returns `nil` when the container can't be resolved — treat /// that as "iCloud required," not as a cue to fall back to local /// storage (see the icloud-sync-engine skill). public func documentsURL() async -> URL? { guard let containerURL = FileManager.default.url(forUbiquityContainerIdentifier: containerIdentifier) else { return nil } let documentsURL = containerURL.appendingPathComponent("Documents", isDirectory: true) try? FileManager.default.createDirectory(at: documentsURL, withIntermediateDirectories: true) return documentsURL } /// File count and total size under `Documents/`, for a diagnostics /// screen's "N documents, X MB" line. Walks the whole tree — fine for /// typical per-user datasets, but it's an O(files) directory walk, not /// something to call in a loop. public func containerStats() async -> (fileCount: Int, totalBytes: Int64)? { guard let documentsURL = await documentsURL() else { return nil } return Self.walk(documentsURL) } /// Synchronous, non-`async` on purpose: `FileManager.DirectoryEnumerator` /// iteration goes through `NSEnumerator.nextObject()`, which Foundation /// marks unavailable directly inside an `async` function body. Keeping /// the `for case let ... in enumerator` loop in a plain sync function /// and calling it from the `async` method above sidesteps that — /// the blocking work still happens off the caller's thread because this /// whole type is an actor. private nonisolated static func walk(_ documentsURL: URL) -> (fileCount: Int, totalBytes: Int64) { // No `.skipsHiddenFiles`: an evicted document exists on disk only as a // hidden "..icloud" placeholder — skipping hidden files makes // evicted documents vanish from the count (a fully-evicted container // would report "0 documents"). Placeholders count as documents; their // on-disk placeholder size stands in for the real size, so totalBytes // reflects local footprint, not logical size. Other hidden files // (.DS_Store etc.) are still skipped by name. guard let enumerator = FileManager.default.enumerator( at: documentsURL, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [] ) else { return (0, 0) } var fileCount = 0 var totalBytes: Int64 = 0 for case let fileURL as URL in enumerator { guard let values = try? fileURL.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey]), values.isDirectory == false else { continue } if fileURL.lastPathComponent.hasPrefix("."), fileURL.pathExtension != "icloud" { continue } fileCount += 1 totalBytes += Int64(values.fileSize ?? 0) } return (fileCount, totalBytes) } }