Persistence Patterns
Data persistence approaches for iOS apps.
Problem
Apps need to persist data across sessions. iOS provides UserDefaults, CoreData, and SwiftData with different complexity and capability trade-offs.
Solution 1: UserDefaults (Simple State)
For small amounts of structured data:
@Observable
class SettingsManager {
private let defaults = UserDefaults.standard
var soundEnabled: Bool {
get { defaults.bool(forKey: "soundEnabled") }
set { defaults.set(newValue, forKey: "soundEnabled") }
}
var highScore: Int {
get { defaults.integer(forKey: "highScore") }
set { defaults.set(newValue, forKey: "highScore") }
}
}With Codable Objects
struct GameProgress: Codable {
var level: Int
var score: Int
var achievements: [String]
}
class ProgressManager {
private let key = "gameProgress"
func save(_ progress: GameProgress) {
if let data = try? JSONEncoder().encode(progress) {
UserDefaults.standard.set(data, forKey: key)
}
}
func load() -> GameProgress? {
guard let data = UserDefaults.standard.data(forKey: key),
let progress = try? JSONDecoder().decode(GameProgress.self, from: data) else {
return nil
}
return progress
}
}Solution 2: AppStorage (SwiftUI Integration)
For settings that directly bind to views:
struct SettingsView: View {
@AppStorage("soundEnabled") private var soundEnabled = true
@AppStorage("darkMode") private var darkMode = false
@AppStorage("username") private var username = ""
var body: some View {
Form {
Toggle("Sound", isOn: $soundEnabled)
Toggle("Dark Mode", isOn: $darkMode)
TextField("Username", text: $username)
}
}
}Solution 3: File-Based (Documents)
For larger data or custom formats:
class DocumentStorage {
private let fileName = "gameData.json"
private var fileURL: URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
.appendingPathComponent(fileName)
}
func save<T: Encodable>(_ data: T) throws {
let encoded = try JSONEncoder().encode(data)
try encoded.write(to: fileURL)
}
func load<T: Decodable>(_ type: T.Type) throws -> T {
let data = try Data(contentsOf: fileURL)
return try JSONDecoder().decode(type, from: data)
}
}Trade-offs
UserDefaults
- Use when: Small data, settings, preferences
- Avoid when: Large datasets, complex relationships
- Limit: ~1MB practical maximum
AppStorage
- Use when: SwiftUI views need direct binding
- Avoid when: Complex data, non-view contexts
File-Based
- Use when: Larger data, need portability
- Avoid when: Relational data, need queries
CoreData/SwiftData
- Use when: Relational data, complex queries
- Avoid when: Simple key-value needs
Design Decisions
Session-Only State
Some apps intentionally don’t persist (Quest-Forge):
- Sessions are short by design
- Reduces architecture complexity
- No save state bugs
Incremental Saves
For games with frequent updates:
func processMove() {
// Only save on significant events
if gameState.isCheckpoint {
saveProgress()
}
}Source
Patterns used across iOS projects for appropriate persistence levels.