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

  1. Is it just settings? Use UserDefaults.
  2. Is it structured data that fits in memory? Use Codable + FileManager.
  3. Do you need complex queries or relationships? Consider Core Data.
  4. 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.