Data Persistence in iOS Apps
Core Decision
Choose the simplest persistence method that solves your problem. Most apps don't need complex databases. Start simple and only add complexity when you have a specific reason.
UserDefaults: For Simple Preferences
When to Use It
Settings, preferences, small pieces of state. Toggle switches, selected themes, user preferences. Anything that's naturally key-value and fits in a few kilobytes.
Example
// Save
UserDefaults.standard.set(true, forKey: "darkModeEnabled")
// Read
let isDarkMode = UserDefaults.standard.bool(forKey: "darkModeEnabled")
When NOT to Use It
Don't store large amounts of data. Don't store sensitive information (use Keychain instead). Don't use it for data that needs structure or relationships. UserDefaults is a dictionary—treat it like one.
Codable with FileManager: For Structured Data
When to Use It
When you have custom types that need to persist. Task lists, notes, journal entries, any structured data that fits in memory. This is often the sweet spot for independent apps—simple enough to implement quickly, powerful enough for most use cases.
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
Simple. No dependencies. Easy to debug (JSON files are human-readable). Version control friendly. You can open the files and see exactly what's stored. Migration is just transforming JSON.
Core Data: For Complex Relationships
When to Use It
When you need relationships between data models. When you need to query subsets of data efficiently. When you're working with large datasets that don't fit in memory. When you need built-in undo/redo.
When NOT to Use It
Don't use Core Data just because it seems "professional." The complexity cost is real. If your data fits in memory and doesn't have complex relationships, Codable with FileManager is probably simpler.
Reality Check
Core Data has a learning curve. Managed object contexts, fetch requests, relationships, migrations—these add complexity. Make sure the benefits justify the cost. For many independent apps, they don't.
CloudKit: For Sync
When to Use It
When users need data on multiple devices. When you want sync without running your own servers. When you're okay with the iCloud ecosystem as a requirement.
Important Considerations
CloudKit is powerful but requires careful error handling. Users might be logged out of iCloud, they might have no storage space, network might be unavailable. Your app must work offline and gracefully handle sync failures. This is more complex than it sounds.
Decision Framework
Start Here
- Is it just settings? Use UserDefaults.
- Is it structured data that fits in memory? Use Codable + FileManager.
- Do you need complex queries or relationships? Consider Core Data.
- Do you need cross-device sync? Add CloudKit or another sync solution.
Migration Path
Start simple. You can always migrate to more complex solutions later. Going from Codable to Core Data is work, but it's manageable. Starting with Core Data "just in case" adds complexity from day one—and you might never need it.
Practical Tips
Always Handle Errors
File saves can fail. Disk can be full. Permissions can be denied. Handle these gracefully. At minimum, log the error. Better: show the user a clear message and give them options (retry, save elsewhere, etc.).
Save Frequently
Auto-save on every change or on a short timer. Never ask users to manually save. Never lose data because the app crashed. Save is cheap—data loss is expensive.
Test Data Loss Scenarios
Kill your app mid-save. Fill up the disk. Test with low storage. Put device in airplane mode. Your persistence layer should handle these without losing user data.
The Goal
Users should never think about how their data is stored. It should just be there, reliably, every time they open the app. That's what good persistence looks like—invisible, reliable, and simple to implement.