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

  1. How much data will the app store?
  2. Does the data need relationships, filtering, or history?
  3. Does it need to sync across devices?
  4. 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.