Swift Data Persistence — UserDefaults, Core Data, SwiftData, and Keychain
In this tutorial, you will learn about Swift Data Persistence. We cover key concepts, practical examples, and best practices to help you master this topic.
Swift data persistence encompasses several technologies that allow iOS and macOS apps to save data between launches, from simple key-value storage with UserDefaults to relational databases with Core Data and the modern SwiftData framework.
What You'll Learn
- UserDefaults for simple key-value storage
- Codable-based JSON file persistence
- Core Data fundamentals and stack setup
- SwiftData (iOS 17+) for modern persistence
- Keychain for secure credential storage
- Choosing the right persistence layer
Why It Matters
Without persistence, all app data is lost when the app closes. User preferences, login tokens, cached API responses, and user-generated content must survive app restarts and device reboots. Choosing the correct persistence Strategy directly affects app performance, security, and user experience.
Real-World Use
A note-taking app uses SwiftData for storing notes with full-text search, UserDefaults for user preferences (font size, theme), and the Keychain for storing the user's authentication token. Each persistence technology handles a specific need at the appropriate level of complexity.
Learning Path
flowchart LR A[Networking
Lesson 20] --> B[Data Persistence
You are here] B --> C[Concurrency
Lesson 22] B --> D[Notifications
Lesson 23] style B fill:#f90,color:#fff
UserDefaults
UserDefaults stores simple key-value pairs: strings, numbers, booleans, dates, and Data objects. It is fast and thread-safe but designed for small amounts of data only.
import Foundation
class SettingsManager {
static let shared = SettingsManager()
private let defaults = UserDefaults.standard
var username: String {
get { defaults.string(forKey: "username") ?? "Guest" }
set { defaults.set(newValue, forKey: "username") }
}
var isDarkMode: Bool {
get { defaults.bool(forKey: "darkMode") }
set { defaults.set(newValue, forKey: "darkMode") }
}
var fontSize: Double {
get {
let value = defaults.double(forKey: "fontSize")
return value == 0 ? 14.0 : value
}
set { defaults.set(newValue, forKey: "fontSize") }
}
var launchCount: Int {
get { defaults.integer(forKey: "launchCount") }
set { defaults.set(newValue, forKey: "launchCount") }
}
func incrementLaunchCount() {
launchCount += 1
}
func reset() {
let domain = Bundle.main.bundleIdentifier!
defaults.removePersistentDomain(forName: domain)
}
}
SettingsManager.shared.username = "Alice"
SettingsManager.shared.isDarkMode = true
SettingsManager.shared.fontSize = 16.0
SettingsManager.shared.incrementLaunchCount()
print("User: \(SettingsManager.shared.username)")
print("Dark mode: \(SettingsManager.shared.isDarkMode)")
print("Font size: \(SettingsManager.shared.fontSize)")
print("Launches: \(SettingsManager.shared.launchCount)")
Output:
User: Alice
Dark mode: true
Font size: 16.0
Launches: 1
UserDefaults automatically synchronizes to disk periodically. Use it for preferences, settings, and small state flags only.
Codable JSON Persistence
For structured data like arrays of model objects, encode to JSON and save to the app's Documents directory.
import Foundation
struct Task: Codable, Identifiable {
let id: UUID
var title: String
var isCompleted: Bool
let createdAt: Date
}
class TaskStorage {
private let fileName = "tasks.json"
private var fileURL: URL {
let documents = FileManager.default.urls(
for: .documentDirectory,
in: .userDomainMask
).first!
return documents.appendingPathComponent(fileName)
}
func save(_ tasks: [Task]) throws {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(tasks)
try data.write(to: fileURL, options: .atomic)
print("Saved \(tasks.count) tasks to \(fileURL.lastPathComponent)")
}
func load() throws -> [Task] {
guard FileManager.default.fileExists(atPath: fileURL.path) else {
return []
}
let data = try Data(contentsOf: fileURL)
let decoder = JSONDecoder()
let tasks = try decoder.decode([Task].self, from: data)
print("Loaded \(tasks.count) tasks")
return tasks
}
func delete() throws {
if FileManager.default.fileExists(atPath: fileURL.path) {
try FileManager.default.removeItem(at: fileURL)
print("Deleted tasks file")
}
}
}
let storage = TaskStorage()
var tasks = [
Task(id: UUID(), title: "Buy groceries", isCompleted: false, createdAt: Date()),
Task(id: UUID(), title: "Finish Swift tutorial", isCompleted: true, createdAt: Date())
]
try storage.save(tasks)
let loadedTasks = try storage.load()
for task in loadedTasks {
let status = task.isCompleted ? "Done" : "Pending"
print(" \(status): \(task.title)")
}
Output:
Saved 2 tasks to tasks.json
Loaded 2 tasks
Pending: Buy groceries
Done: Finish Swift tutorial
This approach works for any Codable type. The data is stored as plain JSON in the app sandbox, making it easy to debug and backup.
Core Data
Core Data is Apple's mature object graph and persistence framework for complex relational data.
import CoreData
class CoreDataStack {
static let shared = CoreDataStack()
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "AppModel")
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Core Data failed: \(error.localizedDescription)")
}
}
return container
}()
var viewContext: NSManagedObjectContext {
return persistentContainer.viewContext
}
func saveContext() {
let context = viewContext
if context.hasChanges {
do {
try context.save()
print("Core Data saved")
} catch {
print("Save failed: \(error.localizedDescription)")
}
}
}
}
// Creating managed objects requires an .xcdatamodeld file with entity definitions.
// This example assumes a "Person" entity with "name" and "age" attributes.
func createSampleData() {
let context = CoreDataStack.shared.viewContext
// In a real project, these would be NSManagedObject subclasses
// generated from the Core Data model editor:
// let person = Person(context: context)
// person.name = "Alice"
// person.age = 30
CoreDataStack.shared.saveContext()
print("Sample data created")
}
func fetchPeople() {
let context = CoreDataStack.shared.viewContext
let request = NSFetchRequest<NSManagedObject>(entityName: "Person")
do {
let people = try context.fetch(request)
print("Fetched \(people.count) people")
for person in people {
let name = person.value(forKey: "name") as? String ?? ""
let age = person.value(forKey: "age") as? Int ?? 0
print(" \(name), age \(age)")
}
} catch {
print("Fetch failed: \(error.localizedDescription)")
}
}
Core Data requires a data model file (.xcdatamodeld) created in Xcode. It supports relationships, Lazy Loading, faulting, and automatic undo management.
SwiftData
SwiftData is Apple's modern persistence framework introduced in iOS 17. It uses Swift macros for a cleaner API.
import SwiftData
import SwiftUI
// @Model macro generates all necessary Core Data-like code
@Model
class Note {
var title: String
var content: String
var createdAt: Date
var isFavorite: Bool
var tags: [String]
init(title: String, content: String, tags: [String] = []) {
self.title = title
self.content = content
self.createdAt = Date()
self.isFavorite = false
self.tags = tags
}
}
// Usage in SwiftUI:
// struct NotesView: View {
// @Query(sort: \Note.createdAt, order: .reverse) var notes: [Note]
// @Environment(\.modelContext) var context
//
// var body: some View {
// List {
// ForEach(notes) { note in
// Text(note.title)
// }
// }
// .toolbar {
// Button("Add") {
// let note = Note(title: "New Note", content: "")
// context.insert(note)
// }
// }
// }
// }
func swiftDataDemo() {
let schema = Schema([Note.self])
let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
let container = try! ModelContainer(for: Note.self, configurations: config)
let context = ModelContext(container)
let note = Note(title: "Hello SwiftData", content: "This is a sample note.")
context.insert(note)
try? context.save()
let descriptor = FetchDescriptor<Note>(
predicate: #Predicate { $0.isFavorite == true },
sortBy: [SortDescriptor(\.createdAt, order: .reverse)]
)
let favorites = try? context.fetch(descriptor)
print("Favorites count: \(favorites?.count ?? 0)")
print("Note saved: \(note.title)")
}
Output: Note saved: Hello SwiftData
SwiftData integrates seamlessly with SwiftUI using @Query and @Model. It eliminates the need for .xcdatamodeld files and manual NSManagedObject subclasses.
Keychain Persistence
The Keychain stores sensitive data like passwords and tokens securely using hardware-backed encryption.
import Foundation
import Security
class KeychainManager {
static let shared = KeychainManager()
private let service = "com.dodatech.app"
func save(key: String, value: String) -> Bool {
guard let data = value.data(using: .utf8) else { return false }
// Delete existing item first
let deleteQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
SecItemDelete(deleteQuery as CFDictionary)
// Add new item
let addQuery: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
let status = SecItemAdd(addQuery as CFDictionary, nil)
return status == errSecSuccess
}
func read(key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess,
let data = result as? Data,
let value = String(data: data, encoding: .utf8) else {
return nil
}
return value
}
func delete(key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrService as String: service,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess
}
}
let keychain = KeychainManager.shared
let saved = keychain.save(key: "authToken", value: "eyJhbGciOiJIUzI1NiIs...")
print("Token saved: \(saved)")
if let token = keychain.read(key: "authToken") {
print("Read token: \(token.prefix(20))...")
}
Output:
Token saved: true
Read token: eyJhbGciOiJIUzI1NiIs...
Never store passwords, tokens, or API keys in UserDefaults. The Keychain is the only secure storage option on iOS.
Choosing the Right Persistence Layer
| Technology | Best For | Limitations |
|---|---|---|
| UserDefaults | Preferences, settings, small flags | Not for structured or large data |
| Codable JSON | Structured data, small to medium datasets | Loads entire file into memory |
| Core Data | Complex relational data, large datasets | Heavy setup, requires model file |
| SwiftData | Modern Swift apps (iOS 17+) | Requires iOS 17 or later |
| Keychain | Passwords, tokens, secrets | Key-value only, slower access |
Common Mistakes
Storing sensitive data in UserDefaults: UserDefaults is plain text in the app sandbox. Never store passwords, tokens, or credit card info there.
Loading large datasets with Codable: JSON decoding loads the entire file into memory. For datasets over a few MB, use Core Data or SQLite.
Forgetting to save Core Data context: Changes in the managed object context are not persisted until
save()is called. If the app crashes before saving, changes are lost.Using SwiftData with older iOS versions: SwiftData requires iOS 17 or later. Check deployment target before adopting it.
Not handling Keychain errors gracefully: Keychain operations can fail due to security policies, device restrictions, or keychain corruption. Always handle the error cases.
Practice Questions
- What is the maximum data size appropriate for UserDefaults?
- Why should you not store authentication tokens in UserDefaults?
- What is the difference between Core Data and SwiftData?
- How does the Keychain encrypt data differently from file-based storage?
- Challenge: Build a
RecentSearchessystem that persists up to 10 recent search strings using UserDefaults. When the 11th search is added, remove the oldest. Then reimplement it using Codable JSON storage.
Mini Project
Create a JournalApp persistence layer with:
- A
JournalEntrystruct withid,title,content,date,mood(enum: happy, neutral, sad) - A
PersistenceManagerclass withsave(_:),load(),delete(_:), andsearch(query:)methods - Use Codable JSON storage for the entries
- Use UserDefaults for app preferences (entry font size, sort order)
- Use Keychain for a simulated "passcode lock" feature
- Print all entries and settings on launch
FAQ
What's Next
After mastering persistence, learn how to manage concurrent operations safely with Concurrency, or handle real-time updates with Notifications.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro