Initial scaffold: context-aware notes app for iOS + macOS

XcodeGen project (Notes / NotesMac / NotesTests), Swift 6 strict concurrency,
iCloud-document storage via IndieSync with a SwiftData cache, automatic
capture context (location, place, time zone, device) on every note,
context-based relevance ranking with a 'Right here, right now' shelf,
tags, search, and the square.and.pencil-on-red app icon.
This commit is contained in:
2026-07-14 20:30:46 -04:00
commit 9b231a1978
42 changed files with 2126 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
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)
}
}
}