How it works
The explanation, with examples you can run.
Settings: UserDefaults
UserDefaults.standard.set(true, forKey: "hasSeenOnboarding")let seen = UserDefaults.standard.bool(forKey: "hasSeenOnboarding") // In SwiftUI, @AppStorage reads and writes the same store and refreshes the view.@AppStorage("sortOrder") private var sortOrder = "date"UserDefaults is a key-value store of property-list types (strings, numbers, dates, data, arrays and dictionaries of them). Writes update memory at once and reach disk asynchronously. It is loaded as a whole, so keep it small, and it is not encrypted: Apple's own documentation says to keep personal or sensitive data out of it. Using it also means declaring a reason in your privacy manifest (see the privacy topic).
Secrets: the Keychain
Passwords, tokens and keys belong in the Keychain (Security framework, SecItemAdd and friends). Items are encrypted, can require the device to be unlocked or biometrics, and can be shared between your apps through an access group. Unlike your container, Keychain items can survive deleting and reinstalling the app, so don't rely on reinstall to clear them.
Files: pick the folder on purpose
| Folder | Backed up | Purged by system | Use for |
|---|---|---|---|
Documents | Yes | No | Files the user creates; can be shown in the Files app |
Library/Application Support | Yes | No | Your app's own data files and databases |
Library/Caches | No | Yes, when space is low | Anything you can download or rebuild |
tmp | No | Yes | Scratch files you delete when done |
let support = try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)try data.write(to: support.appending(path: "drafts.json"), options: [.atomic, .completeFileProtection])Write atomically so a crash or kill mid-write can't leave half a file. Files get data protection from the passcode: by default a file is unreadable until the first unlock after a reboot, and .completeFileProtection locks it whenever the device is locked (which also means you can't read it from a background task while locked). Large re-downloadable files outside Caches can be marked with isExcludedFromBackup.
Records: SwiftData or Core Data
For many related objects that you filter, sort and change, use a database. SwiftData (iOS 17) describes the schema in Swift and is built on Core Data's storage:
@Model final class Trip { var name: String var start: Date @Relationship(deleteRule: .cascade) var stops: [Stop] = [] init(name: String, start: Date) { self.name = name; self.start = start }} struct TripsView: View { @Query(sort: \Trip.start) private var trips: [Trip] // refreshes the view on change @Environment(\.modelContext) private var context var body: some View { List(trips) { Text($0.name) } }}@Model classes are observable, so SwiftUI updates as they change. The container's mainContext is bound to the main actor and autosaves; heavy imports belong in a ModelActor with its own context, passing PersistentIdentifiers between actors rather than model objects. Core Data remains the choice when you need to support iOS 16 and earlier, or features SwiftData doesn't have yet. Neither replaces your server: treat a local database as a cache plus offline edits, and plan how changes sync.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.