Learn Swift - Data Persistence & File I/O
Episode 7 of 23

Learn Swift - Data Persistence & File I/O

This episode covers data storage in Swift: working with FileManager and URL to read and write local files, serialization with the Codable protocol for JSON and plist, lightweight storage with UserDefaults, Keychain for secrets, and an overview of Core Data for complex persistence.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Useful applications store data — and that data must survive after the app is closed. Episode 7 covers data persistence and file I/O in Swift: how to read and write files through FileManager and URL, serialize data with the Codable protocol, use UserDefaults for preferences, and understand when Core Data is needed.

The foundation from this episode will be used in almost every real application: offline caches, user drafts, preferences, and even local databases. Choose the right storage layer for your needs — each layer has its own strengths and costs.

FileManager and File I/O

Working with URLs and Directories

Every file operation in Swift is based on URL. FileManager provides standard locations such as Documents and Caches:

Get the Documents directory
import Foundation
 
let fm = FileManager.default
let docs = try fm.url(for: .documentDirectory, in: .userDomainMask,
                      appropriateFor: nil, create: true)
print(docs.path)

fm.url(for:in:appropriateFor:create:) returns the URL of the app sandbox's document directory. Always use this API instead of hardcoding paths — sandbox locations change between versions and devices.

Reading and Writing Files

Writing and reading text takes just a single call:

Write and read a text file
let catatan = "Ini isi catatan pertama\n"
let fileURL = docs.appendingPathComponent("catatan.txt")
 
try catatan.write(to: fileURL, atomically: true, encoding: .utf8)
let isi = try String(contentsOf: fileURL, encoding: .utf8)
print(isi)

catatan.write(to:atomically:encoding:) saves text atomically — a temporary file is written and then swapped in, so readers never see a half-written file. String(contentsOf:encoding:) reads the entire file contents. Remember that every one of these operations can throw an error that must be handled with try.

Codable for Serialization

Serializable Models

The Codable protocol lets Swift structs be converted to JSON, plist, and other formats automatically. Simply declare conformance to the protocol:

Codable model
struct Pengguna: Codable {
    let nama: String
    let email: String
    let skor: Int
}
 
let pengguna = Pengguna(nama: "Arman", email: "arman@example.com", skor: 100)
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let data = try encoder.encode(pengguna)
print(String(data: data, encoding: .utf8)!)

struct Pengguna: Codable hands all the serialization work to the compiler — as long as all properties are also Codable. JSONEncoder turns an instance into Data, and JSONDecoder does the reverse.

Mapping Key Names

Often JSON keys differ from Swift property names. Use CodingKeys to map them:

Mapping JSON keys
struct Post: Codable {
    let id: Int
    let judul: String
 
    enum CodingKeys: String, CodingKey {
        case id
        case judul = "title"
    }
}

case judul = "title" tells the decoder that the judul property maps from the JSON key title. This mapping is essential when consuming APIs that use a different naming convention (episode 8 will take advantage of it).

UserDefaults and Keychain

UserDefaults for Preferences

UserDefaults is the right place for small pieces of data such as settings and user preferences:

Save preferences
let defaults = UserDefaults.standard
defaults.set("gelap", forKey: "temaAplikasi")
defaults.set(true, forKey: "notifikasiAktif")
 
let tema = defaults.string(forKey: "temaAplikasi") ?? "terang"
print(tema)

defaults.set("gelap", forKey: "temaAplikasi") saves a value that's immediately available on the next launch. UserDefaults is suitable for up to a few kilobytes of data — don't store large documents or long lists here.

Keychain for Secrets

Credentials, tokens, and sensitive data belong in the Keychain, not in UserDefaults or regular files. The Keychain encrypts data and ties it to the device. On Apple platforms, use the Security framework API; on the server side, use environment variables:

Read a secret from the environment
import Foundation
 
let apiKey = ProcessInfo.processInfo.environment["API_KEY"] ?? ""
print("Kunci ada: \(!apiKey.isEmpty)")

ProcessInfo.processInfo.environment["API_KEY"] reads an environment variable — the primary pattern for secrets in server-side Swift and CI. We'll dissect the iOS Keychain fully in episode 11.

Core Data Overview

Complex Persistence

For large relational data, such as thousands of entities with relationships, Core Data is Apple's primary persistence framework. It provides managed data models, fetch-request-based queries, incremental changes, and full integration with SwiftUI through property wrappers like @FetchRequest.

When to choose Core Data:

  • Data has relationships between entities that need to be queried across.
  • The dataset is medium to large, for example thousands of records.
  • You need undo, model versioning, or integration with a cloud database.

Info

A common selection rule: UserDefaults for settings, JSON files for simple data, Keychain for secrets, and Core Data or SQLite for large relational data. There is no single answer — match the layer to your app's data access patterns.

Choosing a Persistence Strategy

A summary of the decisions you'll often face:

  • Small preferences: UserDefaults.
  • User documents: files in Documents via FileManager.
  • Temporary/offline data: files in Caches.
  • Secrets and tokens: Keychain or environment variables.
  • Large relational data: Core Data or SQLite.

Start from the simplest layer that meets your needs, then move up a layer when problems arise. Premature optimization at the persistence layer only adds unnecessary complexity.

Closing

Key takeaways:

  • All file operations are based on URL and FileManager, always through official sandbox locations.
  • Writing files atomically avoids half-written files.
  • Codable simplifies JSON and plist serialization, with CodingKeys for mapping.
  • UserDefaults for small data; Keychain or environment for secrets.
  • Core Data serves large, complex relational data.
  • Choose the simplest persistence layer that meets your needs.

In the next episode, episode 8, we'll cover networking and API integration — HTTP requests with URLSession, JSON parsing with Codable along with response error handling, modern concurrency with async/await, and networking best practices such as caching, retries, and request timeouts. Your app will start talking to the outside world!

Learn Swift - Data Persistence & File I/O | Learn Swift