Files and persistence

Save Codable data atomically

Encode one small model, write it atomically, and replace the previous file only after the new bytes are ready.

12 minute lesson

~~~

Our notes need to survive a relaunch. For a small local app you do not need a database — a single JSON file, written carefully, carries you a long way.

Since Note already conforms to Codable, encoding the whole array is two lines:

func save(_ notes: [Note], to fileURL: URL) throws {
  let data = try JSONEncoder().encode(notes)
  try data.write(to: fileURL, options: .atomic)
}

The .atomic option is the part people skip, and it is the part that matters. Without it, write streams bytes straight into the destination file. If the app crashes or the Mac loses power halfway through, the file is left half-written — the old data is gone and the new data is garbage.

An atomic write goes through a temporary file. The full new content is written beside the destination, and only then renamed into place. The rename is a single filesystem operation: at every moment the path holds either the complete old file or the complete new one, never a mix.

Loading needs more care than saving, because “no file” and “broken file” are different situations:

func load(from fileURL: URL) throws -> [Note] {
  let data: Data
  do {
    data = try Data(contentsOf: fileURL)
  } catch CocoaError.fileReadNoSuchFile {
    return []
  }
  return try JSONDecoder().decode([Note].self, from: data)
}

A missing file is normal — it is the first launch, and an empty list is the right answer. A file that exists but fails to decode is not normal. Let that error propagate and show the user something, because it means their data is there but unreadable.

Here is the mistake that destroys data: wrapping the whole load in try? and falling back to []. Decoding fails for some reason, the app starts “fresh” with zero notes, and the next autosave writes that empty array over the user’s file. The corrupt-but-recoverable data is now actually gone. Distinguish the errors, and never let a failed read feed the next write.

When you later change the Note structure, copy the old file to a backup name before migrating. A migration that fails halfway with no backup is the same story with extra steps.

Verify with a crash test. Save a few notes, find the JSON in Application Support, and open it — it should be readable and complete. Then force-quit the app mid-use a few times. The file should always parse. If you ever find half a JSON document in there, an unatomic write slipped in somewhere.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →