# Data Persistence SwiftData, Core Data, and file-based storage for iOS apps. ## SwiftData (iOS 17+) ### Model Definition ```swift import SwiftData @Model class Item { var name: String var createdAt: Date var isCompleted: Bool var priority: Int @Relationship(deleteRule: .cascade) var tasks: [Task] @Relationship(inverse: \Category.items) var category: Category? init(name: String, priority: Int = 0) { self.name = name self.createdAt = Date() self.isCompleted = false self.priority = priority self.tasks = [] } } @Model class Task { var title: String var isCompleted: Bool init(title: String) { self.title = title self.isCompleted = false } } @Model class Category { var name: String var items: [Item] init(name: String) { self.name = name self.items = [] } } ``` ### Container Setup ```swift @main struct MyApp: App { var body: some Scene { WindowGroup { ContentView() } .modelContainer(for: [Item.self, Category.self]) } } ``` ### Querying Data ```swift struct ItemList: View { // Basic query @Query private var items: [Item] // Sorted query @Query(sort: \Item.createdAt, order: .reverse) private var sortedItems: [Item] // Filtered query @Query(filter: #Predicate { $0.isCompleted == false }) private var incompleteItems: [Item] // Complex query @Query( filter: #Predicate { !$0.isCompleted && $0.priority > 5 }, sort: [ SortDescriptor(\Item.priority, order: .reverse), SortDescriptor(\Item.createdAt) ] ) private var highPriorityItems: [Item] var body: some View { List(items) { item in ItemRow(item: item) } } } ``` ### CRUD Operations ```swift struct ItemList: View { @Query private var items: [Item] @Environment(\.modelContext) private var context var body: some View { List { ForEach(items) { item in ItemRow(item: item) } .onDelete(perform: delete) } .toolbar { Button("Add", action: addItem) } } private func addItem() { let item = Item(name: "New Item") context.insert(item) // Auto-saves } private func delete(at offsets: IndexSet) { for index in offsets { context.delete(items[index]) } } } ``` ### Custom Container Configuration ```swift @main struct MyApp: App { let container: ModelContainer init() { let schema = Schema([Item.self, Category.self]) let config = ModelConfiguration( schema: schema, isStoredInMemoryOnly: false, allowsSave: true, groupContainer: .identifier("group.com.yourcompany.app") ) do { container = try ModelContainer(for: schema, configurations: config) } catch { fatalError("Failed to configure SwiftData container: \(error)") } } var body: some Scene { WindowGroup { ContentView() } .modelContainer(container) } } ``` ### iCloud Sync SwiftData syncs automatically with iCloud when: 1. App has iCloud capability 2. User is signed into iCloud 3. Container uses CloudKit ```swift let config = ModelConfiguration( cloudKitDatabase: .automatic ) ``` ## Core Data (All iOS Versions) ### Stack Setup ```swift class CoreDataStack { static let shared = CoreDataStack() lazy var persistentContainer: NSPersistentContainer = { let container = NSPersistentContainer(name: "MyApp") // Enable cloud sync guard let description = container.persistentStoreDescriptions.first else { fatalError("No persistent store description") } description.cloudKitContainerOptions = NSPersistentCloudKitContainerOptions( containerIdentifier: "iCloud.com.yourcompany.app" ) container.loadPersistentStores { description, error in if let error = error { fatalError("Core Data failed to load: \(error)") } } container.viewContext.automaticallyMergesChangesFromParent = true container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy return container }() var viewContext: NSManagedObjectContext { persistentContainer.viewContext } func saveContext() { let context = viewContext if context.hasChanges { do { try context.save() } catch { print("Failed to save context: \(error)") } } } } ``` ### With SwiftUI ```swift @main struct MyApp: App { let coreDataStack = CoreDataStack.shared var body: some Scene { WindowGroup { ContentView() .environment(\.managedObjectContext, coreDataStack.viewContext) } } } struct ItemList: View { @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.createdAt, ascending: false)], predicate: NSPredicate(format: "isCompleted == NO") ) private var items: FetchedResults @Environment(\.managedObjectContext) private var context var body: some View { List(items) { item in ItemRow(item: item) } } } ``` ## File-Based Storage ### Codable Models ```swift struct UserSettings: Codable { var theme: Theme var fontSize: Int var notificationsEnabled: Bool enum Theme: String, Codable { case light, dark, system } } class SettingsStore { private let fileURL: URL init() { let documentsDirectory = FileManager.default.urls( for: .documentDirectory, in: .userDomainMask ).first! fileURL = documentsDirectory.appendingPathComponent("settings.json") } func load() -> UserSettings { guard let data = try? Data(contentsOf: fileURL), let settings = try? JSONDecoder().decode(UserSettings.self, from: data) else { return UserSettings(theme: .system, fontSize: 16, notificationsEnabled: true) } return settings } func save(_ settings: UserSettings) throws { let data = try JSONEncoder().encode(settings) try data.write(to: fileURL) } } ``` ### Document Directory Paths ```swift extension FileManager { var documentsDirectory: URL { urls(for: .documentDirectory, in: .userDomainMask).first! } var cachesDirectory: URL { urls(for: .cachesDirectory, in: .userDomainMask).first! } var applicationSupportDirectory: URL { let url = urls(for: .applicationSupportDirectory, in: .userDomainMask).first! try? createDirectory(at: url, withIntermediateDirectories: true) return url } } ``` ## UserDefaults ### Basic Usage ```swift // Save UserDefaults.standard.set("value", forKey: "key") UserDefaults.standard.set(true, forKey: "hasCompletedOnboarding") // Load let value = UserDefaults.standard.string(forKey: "key") let hasCompletedOnboarding = UserDefaults.standard.bool(forKey: "hasCompletedOnboarding") ``` ### @AppStorage ```swift struct SettingsView: View { @AppStorage("fontSize") private var fontSize = 16 @AppStorage("isDarkMode") private var isDarkMode = false @AppStorage("username") private var username = "" var body: some View { Form { Stepper("Font Size: \(fontSize)", value: $fontSize, in: 12...24) Toggle("Dark Mode", isOn: $isDarkMode) TextField("Username", text: $username) } } } ``` ### Custom Codable Storage ```swift extension UserDefaults { func set(_ value: T, forKey key: String) { if let data = try? JSONEncoder().encode(value) { set(data, forKey: key) } } func get(_ type: T.Type, forKey key: String) -> T? { guard let data = data(forKey: key) else { return nil } return try? JSONDecoder().decode(type, from: data) } } // Usage UserDefaults.standard.set(userProfile, forKey: "userProfile") let profile = UserDefaults.standard.get(UserProfile.self, forKey: "userProfile") ``` ## Keychain (Sensitive Data) ### Simple Wrapper ```swift import Security class KeychainService { enum KeychainError: Error { case saveFailed(OSStatus) case loadFailed(OSStatus) case deleteFailed(OSStatus) case dataConversionError } func save(_ data: Data, for key: String) throws { // Delete existing try? delete(key) let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked ] let status = SecItemAdd(query as CFDictionary, nil) guard status == errSecSuccess else { throw KeychainError.saveFailed(status) } } func load(_ key: String) throws -> Data { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: true ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess else { throw KeychainError.loadFailed(status) } guard let data = result as? Data else { throw KeychainError.dataConversionError } return data } func delete(_ key: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key ] let status = SecItemDelete(query as CFDictionary) guard status == errSecSuccess || status == errSecItemNotFound else { throw KeychainError.deleteFailed(status) } } } // String convenience extension KeychainService { func saveString(_ value: String, for key: String) throws { guard let data = value.data(using: .utf8) else { throw KeychainError.dataConversionError } try save(data, for: key) } func loadString(_ key: String) throws -> String { let data = try load(key) guard let string = String(data: data, encoding: .utf8) else { throw KeychainError.dataConversionError } return string } } ``` ### Usage ```swift let keychain = KeychainService() // Save API token try keychain.saveString(token, for: "apiToken") // Load API token let token = try keychain.loadString("apiToken") // Delete on logout try keychain.delete("apiToken") ``` ## Migration Strategies ### SwiftData Migrations ```swift enum SchemaV1: VersionedSchema { static var versionIdentifier = Schema.Version(1, 0, 0) static var models: [any PersistentModel.Type] { [Item.self] } @Model class Item { var name: String init(name: String) { self.name = name } } } enum SchemaV2: VersionedSchema { static var versionIdentifier = Schema.Version(2, 0, 0) static var models: [any PersistentModel.Type] { [Item.self] } @Model class Item { var name: String var createdAt: Date // New field init(name: String) { self.name = name self.createdAt = Date() } } } enum MigrationPlan: SchemaMigrationPlan { static var schemas: [any VersionedSchema.Type] { [SchemaV1.self, SchemaV2.self] } static var stages: [MigrationStage] { [migrateV1toV2] } static let migrateV1toV2 = MigrationStage.lightweight( fromVersion: SchemaV1.self, toVersion: SchemaV2.self ) } ```