Store data on the device with the right tool: UserDefaults for small settings, Keychain for secrets, and Core Data or SwiftData for structured, queryable, relational data — plus the file system for blobs.
UserDefaults is a simple key-value store for small pieces of data — user preferences, flags, a saved sort order. It is easy and synchronous, but it is not secure and not for large or structured data. In SwiftUI you often read it via @AppStorage. Use it for the little things and reach for a real database when data grows.
// direct API
UserDefaults.standard.set(true, forKey: "hasOnboarded")
let seen = UserDefaults.standard.bool(forKey: "hasOnboarded")
// in SwiftUI, @AppStorage reads/writes UserDefaults reactively:
struct SettingsView: View {
@AppStorage("darkMode") private var darkMode = false
var body: some View { Toggle("Dark mode", isOn: $darkMode) }
}
// good for small preferences/flags. NOT for secrets (not encrypted) or large
// structured data (use Keychain / Core Data respectively).Sensitive data — auth tokens, passwords, keys — must never go in UserDefaults, which is unencrypted. The Keychain is the secure, encrypted store for secrets, protected by the device and optionally biometrics. Its C-based API is verbose, so most teams use a thin wrapper, but the rule is absolute: secrets go in the Keychain, nothing else.
Secrets → Keychain only required — Never store tokens, passwords, or keys in UserDefaults or plain files — they are not encrypted. The Keychain is the only appropriate store for secrets.import Security
func saveToken(_ token: String) {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "authToken",
kSecValueData as String: Data(token.utf8)
]
SecItemDelete(query as CFDictionary) // replace if present
SecItemAdd(query as CFDictionary, nil) // store encrypted
}
// verbose C API -> most teams use a small wrapper. The RULE: tokens/passwords/
// keys go in the Keychain (encrypted), never UserDefaults or plain files.For structured, queryable, relational data — a list of records you filter, sort, and relate — you use a real object database. Core Data is the mature framework; SwiftData is Apple’s modern, Swift-native layer over it that models entities as annotated classes with far less boilerplate. Either gives you persistence, querying, and change tracking that integrates with SwiftUI.
import SwiftData
@Model // SwiftData: an entity, persisted automatically
final class Task {
var title: String
var isDone: Bool
init(title: String, isDone: Bool = false) { self.title = title; self.isDone = isDone }
}
struct TaskListView: View {
@Query(sort: \.title) private var tasks: [Task] // live query -> auto-updates UI
@Environment(\.modelContext) private var context
var body: some View {
List(tasks) { Text($0.title) }
}
func add() { context.insert(Task(title: "New")) } // insert -> @Query re-emits
}
// SwiftData (@Model + @Query) is the modern layer over Core Data — far less
// boilerplate, and @Query updates the UI reactively.Match the tool to the data: UserDefaults for small settings, Keychain for secrets, Core Data/SwiftData for structured relational data, and the file system for large binary blobs like images or downloads (store the file on disk, its path in the database). Picking the right store keeps data access simple, secure, and correct.
Which persistence?
small key-value settings -> UserDefaults / @AppStorage
secrets (tokens, keys) -> Keychain (encrypted) — never anywhere else
structured / relational -> Core Data or SwiftData (query, sort, relations)
"a list of records I filter + sort, observed live"
large binary blobs -> the file system (Documents dir)
store the FILE on disk, its PATH in the database
Right tool per shape of data = simple, secure, correct persistence.