import CoreData enum PersistenceControllerError: Error { case encryptionKeyUnavailable case persistentStoreMissing case rekeyingUnavailable } final class PersistenceController { static let shared: PersistenceController = { do { return try PersistenceController() } catch { fatalError("Failed to initialize PersistenceController: \(error)") } }() let container: NSPersistentContainer private let keyManager: DatabaseEncryptionKeyManager var viewContext: NSManagedObjectContext { container.viewContext } init( inMemory: Bool = false, keyManager: DatabaseEncryptionKeyManager = .shared, fileManager: FileManager = .default ) throws { self.keyManager = keyManager let modelName = "YobbleDataModel" guard let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd"), let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL) else { fatalError("Unable to load Core Data model \(modelName)") } container = NSPersistentContainer(name: modelName, managedObjectModel: managedObjectModel) let description = container.persistentStoreDescriptions.first ?? NSPersistentStoreDescription() description.type = NSSQLiteStoreType if inMemory { description.url = URL(fileURLWithPath: "/dev/null") } else { description.url = try Self.makeStoreURL(fileManager: fileManager, fileName: "\(modelName).sqlite") } description.shouldInferMappingModelAutomatically = true description.shouldMigrateStoreAutomatically = true let key: String do { key = try keyManager.currentKey() } catch { throw PersistenceControllerError.encryptionKeyUnavailable } let pragmas: [String: String] = [ "journal_mode": "WAL", "cipher_page_size": "4096", "key": key ] description.setOption(pragmas as NSDictionary, forKey: NSSQLitePragmasOption) description.setOption(FileProtectionType.completeUntilFirstUserAuthentication as NSObject, forKey: NSPersistentStoreFileProtectionKey) container.persistentStoreDescriptions = [description] container.loadPersistentStores { _, error in if let error { fatalError("Unresolved error \(error)") } } container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy container.viewContext.automaticallyMergesChangesFromParent = true } func newBackgroundContext() -> NSManagedObjectContext { container.newBackgroundContext() } /// Placeholder for a future rekey flow once a password-based key is available. /// /// On iOS 16 with SQLCipher you typically re-encrypt by running `PRAGMA rekey` via a raw /// SQLite handle and then persisting the new key to the key manager. This helper keeps /// the signature in place for when that flow is implemented. func rekeyStore(to newKey: String) throws { throw PersistenceControllerError.rekeyingUnavailable } private static func makeStoreURL(fileManager: FileManager, fileName: String) throws -> URL { guard let baseURL = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first else { fatalError("Unable to resolve Application Support directory") } if !fileManager.fileExists(atPath: baseURL.path) { try fileManager.createDirectory(at: baseURL, withIntermediateDirectories: true) } return baseURL.appendingPathComponent(fileName) } }