import Foundation import SwiftData /// Creates the SwiftData cache container. The store is disposable: schema /// changes and iCloud account changes wipe it, and `SyncEngine` rebuilds it /// from the files (an empty cache is the entire contract between the two). enum NotesModelContainer { /// Bump whenever the cache schema changes — wipes and rebuilds from files. static let cacheSchemaVersion = 1 private static let cacheSchemaVersionKey = "Notes.cacheSchemaVersion" private static let identityTokenKey = "Notes.iCloudIdentityToken" private static var storeURL: URL { URL.applicationSupportDirectory.appending(path: "Notes.store") } static func make(inMemory: Bool = false) -> ModelContainer { let schema = Schema([NoteEntity.self]) if !inMemory { try? FileManager.default.createDirectory( at: storeURL.deletingLastPathComponent(), withIntermediateDirectories: true) wipeIfNeeded() wipeIfAccountChanged() do { // `cloudKitDatabase: .none` is load-bearing: the parameter // defaults to `.automatic`, which — because the app is // iCloud-entitled — would silently mirror the cache to // CloudKit on top of the file-based sync. let config = ModelConfiguration(schema: schema, url: storeURL, cloudKitDatabase: .none) let container = try ModelContainer(for: schema, configurations: [config]) UserDefaults.standard.set(cacheSchemaVersion, forKey: cacheSchemaVersionKey) return container } catch { print("Notes: ModelContainer creation failed at \(storeURL.path): \(error). Falling back to in-memory.") } } // In-memory: screenshot/test mode, or last resort so the app still // launches (degraded, cache-only for the session) rather than crashing. do { let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: true, cloudKitDatabase: .none) return try ModelContainer(for: schema, configurations: [config]) } catch { fatalError("Notes: could not create even an in-memory ModelContainer: \(error)") } } private static func wipeIfNeeded() { let stored = UserDefaults.standard.integer(forKey: cacheSchemaVersionKey) guard stored < cacheSchemaVersion else { return } wipeStore() } /// Wipes when the signed-in iCloud account's ubiquity identity token /// differs from the one the cache was built for. A nil token is not a /// change — it's legitimately nil for the same account early in launch. private static func wipeIfAccountChanged() { guard let current = currentIdentityTokenData() else { return } let stored = UserDefaults.standard.data(forKey: identityTokenKey) guard current != stored else { return } wipeStore() UserDefaults.standard.set(current, forKey: identityTokenKey) } private static func currentIdentityTokenData() -> Data? { guard let token = FileManager.default.ubiquityIdentityToken else { return nil } return try? NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: false) } /// Removes the on-disk store and its SQLite WAL/SHM sidecars. Safe /// because the store is a rebuildable cache. private static func wipeStore() { for url in [storeURL, URL(fileURLWithPath: storeURL.path + "-wal"), URL(fileURLWithPath: storeURL.path + "-shm")] { try? FileManager.default.removeItem(at: url) } } }