Data Persistence in iOS
Choose storage based on the product, not the trend
Persistence is the part of the app that remembers. The right choice depends on the size of the data, how it changes, and whether it needs to sync.
UserDefaults
When to use it
Use UserDefaults for small settings, preferences, flags, and simple values that do not need relationships or complex queries.
Example
// Save
UserDefaults.standard.set(true, forKey: "darkModeEnabled")
// Read
let isDarkMode = UserDefaults.standard.bool(forKey: "darkModeEnabled")
What not to store
Do not use UserDefaults as a database for large lists, sensitive data, or anything that needs structured search and migration.
Codable files
When files are enough
Codable files work well for small local collections, drafts, lightweight documents, and apps where data shape stays simple.
Example
struct Note: Codable, Identifiable {
let id: UUID
var title: String
var content: String
var createdAt: Date
}
// Save
func saveNotes(_ notes: [Note]) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(notes) {
let url = getDocumentsDirectory().appendingPathComponent("notes.json")
try? encoded.write(to: url)
}
}
// Load
func loadNotes() -> [Note] {
let url = getDocumentsDirectory().appendingPathComponent("notes.json")
guard let data = try? Data(contentsOf: url) else { return [] }
let decoder = JSONDecoder()
return (try? decoder.decode([Note].self, from: data)) ?? []
}
func getDocumentsDirectory() -> URL {
FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
Benefits
Files are transparent, easy to debug, and often enough for a focused MVP before a heavier persistence layer is justified.
Core Data
When Core Data fits
Use Core Data when you need relationships, larger collections, filtering, sorting, migration, and more serious local data management.
When it may be too much
Core Data can add unnecessary complexity if the app only stores a few simple settings or documents.
The practical reality
Core Data is powerful, but it deserves clear models and testing. It should solve a real product need, not just sound professional.
CloudKit
When sync matters
CloudKit is useful when users expect data across devices or when the app benefits from Apple-account based syncing.
What to consider
Sync introduces conflict, latency, account, and support questions. Those questions should be part of the product scope.
Decision framework
Start with four questions
- How much data will the app store?
- Does the data need relationships, filtering, or history?
- Does it need to sync across devices?
- What migration risk is acceptable for the first version?
Plan migration early
Even simple apps change. Leave yourself a path to move from lightweight storage to a stronger model if the product grows.
Practical tips
Handle errors visibly
Silent save failures destroy trust. Show recovery paths and log enough detail to debug real issues.
Save deliberately
Decide when data is saved: immediately, after a user action, or as part of a sync cycle. Ambiguity causes lost work.
Design against data loss
Backups, migration tests, and safe defaults matter more than clever architecture when users rely on the data.
The goal
The best persistence choice is boring, dependable, and proportionate to the product. Start simple, but do not paint the app into a corner.
Need to make these tradeoffs in a real product?
Persistence choices are rarely isolated decisions. They sit inside the wider iOS product scope, performance expectations, and release plan. Shawn Studio can help make those calls inside a real build and ship them cleanly.