- Schema v2: Note.AudioInfo, .m4a sidecar next to the note JSON in iCloud - AudioRecorderService (iOS/macOS) + SpeechAnalyzer transcription pipeline with enabled-language set and confidence-based auto-pick, re-transcribe menu - Settings > Transcription > Languages with asset download/reserve handling - watchOS app + accessory complication: one-button recorder, WCSession transferFile to phone, WatchInbox staging, direct-upsert ingestion - Player UI, transcription status chips, quick-capture mic in notes list - Version 0.2, changelog + README updated
81 lines
3.7 KiB
Swift
81 lines
3.7 KiB
Swift
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.
|
|
/// v2 added the flattened audio columns on `NoteEntity`.
|
|
static let cacheSchemaVersion = 2
|
|
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)
|
|
}
|
|
}
|
|
}
|