Irbisa · Cheatsheet
iOS Persistence
UserDefaults, Keychain, Core Data, SwiftData, FileManager
What persistence options does iOS give you, and how do you choose between them?
What is
UserDefaultsfor, and what must never go in it?let defaults = UserDefaults.standarddefaults.set(true, forKey: "hasSeenOnboarding")defaults.set(["swift", "ios"], forKey: "recentTags") // array of plist types: fine struct Filter: Codable { var sort: String…This toggle should be on by default, yet the sound stays off on a fresh install. Why, and how do you fix it?
struct SettingsView: View { @AppStorage("soundEnabled") private var soundEnabled = true var body: some View { Toggle("Sound", isOn: $soundEnabled) }} final class SoundPlayer { func play(_ name: String) { guard UserDefaults.standard.bool(forKey: "soundEnabled") else { return } // ... plays the sound }}Where do you store an auth token on iOS, and why not in
UserDefaults?enum TokenStore { private static var base: [String: Any] { [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "com.example.auth", kSecAttrAccount as String: "accessToken"] }…Does Keychain data survive deleting the app, and what do accessibility classes and access groups control?
What are
Documents,Application Support,Cachesandtmpfor, and which of them are backed up or can be purged?// Re-downloadable media: caches, purgeable, not backed uplet cachesURL = URL.cachesDirectory.appending(path: "videos", directoryHint: .isDirectory)try FileManager.default.createDirectory(at: cachesURL, withIntermediateDirectories: true)let videoURL = cachesURL.appending(path: "\(assetID).mp4") // Moving a finished download into place beats copying the bytes…NSCoding/NSKeyedArchivervsCodable: what is the difference, and which do you use today?// Codable: synthesised by the compiler, works for a structstruct Settings: Codable { var theme: String var fontSize: Int}let json = try JSONEncoder().encode(Settings(theme: "dark", fontSize: 16))…The app was killed mid-write and the settings file came back as garbage on the next launch. How do you make a file write crash-safe?
// wrong: truncating the live file, a crash here leaves half a documentlet handle = try FileHandle(forWritingTo: url)try handle.truncate(atOffset: 0)try handle.write(contentsOf: JSONEncoder().encode(settings))try handle.close()…After a restore from a backup on a new phone, every saved photo is missing, although the files are in
Documentsand the paths are in the database. What broke?// wrong: the container UUID can change after a restore or migrationlet fileURL = URL.documentsDirectory.appending(path: "avatar.jpg")photo.storedPath = fileURL.path() // /var/mobile/Containers/Data/Application/<UUID>/… // right: persist a name relative to a directory you can rebuildphoto.fileName = "avatar.jpg"…What are the parts of the Core Data stack, and what does each one do?
final class Persistence { let container: NSPersistentContainer init(inMemory: Bool = false) { container = NSPersistentContainer(name: "Model") // loads Model.momd if inMemory {…SwiftData or Core Data: how are they related, and which do you choose for a new app?
How does a list stay in sync with the store:
NSFetchedResultsControllerin UIKit,@FetchRequestand@Queryin SwiftUI?// SwiftData: the filter comes in through init, because a @Query is fixed once builtstruct NotesList: View { @Query private var notes: [Note] init(search: String) { let filter = #Predicate<Note> { search.isEmpty || $0.title.localizedStandardContains(search) }…Find the bug: the customer search crashes for some users while they type a name.
func searchCustomers(matching text: String, in context: NSManagedObjectContext) throws -> [Customer] { let request = Customer.fetchRequest() request.predicate = NSPredicate(format: "name CONTAINS[cd] '\(text)'") request.sortDescriptors = [NSSortDescriptor(key: "name", ascending: true)] request.fetchLimit = 50 do { return try context.fetch(request) } catch { print("Search failed: \(error)") return [] }}Deleting a playlist leaves orphaned tracks behind. How do delete rules and inverses work?
@Modelfinal class Playlist { var name: String // right: cascade deletes the tracks; the default (nullify) leaves them with playlist == nil @Relationship(deleteRule: .cascade, inverse: \Track.playlist)…This code crashes now and then with a Core Data threading error. What is wrong, and how do you pass an object to another context?
func markAsRead(_ article: Article) { // article was fetched from container.viewContext on the main thread let background = container.newBackgroundContext() background.perform { article.isRead = true article.readAt = .now try? background.save() }}Importing twenty thousand records freezes the UI. How do you write to the store off the main thread?
@ModelActoractor ImportActor { func importPosts(_ dtos: [PostDTO]) throws { modelContext.autosaveEnabled = false for start in stride(from: 0, to: dtos.count, by: 500) { for dto in dtos[start..<min(start + 500, dtos.count)] {…Every background sync doubles the number of rows in the table. How do you make the import idempotent?
// SwiftData: the server id is the identity, and insert becomes an upsert@Attribute(.unique) var serverID: String // inside the @Model class// a known serverID is updated in place on insert and save, not duplicated // CloudKit-synced model: no .unique allowed, so dedupe by hand in ONE fetch@Model final class SyncedArticle {…How do you migrate a Core Data or SwiftData store when the schema changes between releases?
enum SchemaV1: VersionedSchema { static let versionIdentifier = Schema.Version(1, 0, 0) static var models: [any PersistentModel.Type] { [Note.self] } @Model final class Note { var title: String…How do you unit test code that persists through SwiftData or Core Data?
@MainActorprivate func makeContainer() throws -> ModelContainer { let configuration = ModelConfiguration(isStoredInMemoryOnly: true) return try ModelContainer(for: Note.self, Folder.self, configurations: configuration)}…Your widget needs the user's data. How do you share a store between the app and its extensions?
let groupID = "group.com.example.notes"let containerURL = FileManager.default .containerURL(forSecurityApplicationGroupIdentifier: groupID)!let storeURL = containerURL.appending(path: "Notes.store") // Shared SwiftData container: identical code in app and widget target…Logging out has to leave nothing of the user behind on the device. What does deleting their data actually involve?
When would you choose SQLite through GRDB, or Realm, over Core Data or SwiftData?
A list backed by @Query gets slower as the table grows. What do you check first?
// Slow: everything is fetched, then filtered and sorted in Swiftlet all = try context.fetch(FetchDescriptor<Message>())let recent = all.filter { $0.isUnread && $0.folderName == folderName } .sorted { $0.sentAt > $1.sentAt } // Fast: predicate translated to SQL, sorted on an indexed column, paged…Two screens edit the same record, both save without an error, and one edit quietly disappears. Where did it go?
// default policy: the save throws instead of guessingviewContext.mergePolicy = NSMergePolicy.error do { try viewContext.save()} catch let error as NSError where error.code == NSManagedObjectMergeError {…The team wants iCloud sync on models you already shipped. What changes, and what will bite you?
@Modelfinal class Note { // Every attribute optional or defaulted: CloudKit has no required columns var title: String = "" var body: String = "" var updatedAt: Date = Date()…