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.

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.
Every file operation in Swift is based on URL. FileManager provides standard locations such as Documents and Caches:
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.
Writing and reading text takes just a single call:
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.
The Codable protocol lets Swift structs be converted to JSON, plist, and other formats automatically. Simply declare conformance to the protocol:
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.
Often JSON keys differ from Swift property names. Use CodingKeys to map them:
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 is the right place for small pieces of data such as settings and user 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.
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:
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.
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:
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.
A summary of the decisions you'll often face:
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.
Key takeaways:
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!