Swift: Swift File and Data Operations
Data persistence is a core capability of apps -- from saving user settings to caching network data. Mastering file I/O and encoding/decoding ensures your data survives app restarts.
1. What You'll Learn
- Using FileManager to create, read, write, and manage files
- JSON encoding/decoding for custom types using the Codable protocol
- Advanced configuration of JSONEncoder and JSONDecoder
- Storing user preferences with UserDefaults
- The file sandbox mechanism and common directory path retrieval
2. An Indie Developer's Real Story
(1) Pain Point: All Data Gone After App Restart
Alice launched her first iOS to-do app. Users loved it, but there was one fatal flaw: close the app and reopen it, and all to-do items were gone. Users left one-star reviews on the App Store.
// Wrong approach: data only lives in memory
var todos: [TodoItem] = [
TodoItem(title: "Buy coffee"),
TodoItem(title: "Write weekly report"),
]
Alice's to-do data was stored in an array. When the app closed, memory was freed, and the data was lost.
(2) The Codable + FileManager Solution
struct TodoItem: Codable {
let title: String
var isDone: false
}
let encoder = JSONEncoder()
if let data = try? encoder.encode(todos) {
let url = FileManager.default.documentsDirectory.appendingPathComponent("todos.json")
try? data.write(to: url)
}
On the next launch, read from the file -- data is permanently saved.
(3) Benefits: User Data Never Lost
| Dimension | In-Memory Storage | File Persistence |
|---|---|---|
| App restart | Data lost | Data restored |
| Storage capacity | RAM limited | Disk space |
| Data sharing | Not supported | Exportable/backup |
| Offline use | Not dependent | Fully offline |
| Restore speed | Instant | Millisecond-level |
3. FileManager File Management
FileManager is Swift's file system interface, providing operations like creating, reading, moving, and deleting files.
(1) Common Directories
graph TB
A["FileManager.default"] --> B["documentsDirectory"]
A --> C["cachesDirectory"]
A --> D["temporaryDirectory"]
B --> E["User data, backed up to iCloud"]
C --> F["Cache files, system may purge"]
D --> G["Temporary files, deleted at any time"]
| Directory | Purpose | Backed Up? | System Purges? |
|---|---|---|---|
| documentsDirectory | User documents and data | Yes | No |
| cachesDirectory | Cache files | No | Yes |
| temporaryDirectory | Temporary files | No | Yes, anytime |
▶ Example: Getting and Creating Directories
// ============================================
// Getting common directory paths and creating subdirectories
// ============================================
import Foundation
let fm = FileManager.default
// 1. Get the documents directory
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
print("Documents directory: \(docs.path)")
// 2. Get the caches directory
let caches = fm.urls(for: .cachesDirectory, in: .userDomainMask).first!
print("Caches directory: \(caches.path)")
// 3. Create a subdirectory
let dataDir = docs.appendingPathComponent("MyAppData")
if !fm.fileExists(atPath: dataDir.path) {
try? fm.createDirectory(at: dataDir, withIntermediateDirectories: true)
print("Created directory: \(dataDir.path)")
}
Output:
TEXT 📖 Display onlyDocuments directory: /Users/alice/Library/Developer/.../Documents Caches directory: /Users/alice/Library/Developer/.../Caches Created directory: /Users/alice/Library/Developer/.../Documents/MyAppData
(2) Writing and Reading Files
FileManager provides basic methods for file operations, combined with Data's write and init(contentsOf:) for file I/O.
▶ Example: Writing and Reading a Text File
// ============================================
// Writing and reading a text file
// ============================================
import Foundation
let fm = FileManager.default
let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first!
// 1. Write text
let fileURL = docs.appendingPathComponent("note.txt")
let content = "Hello, Swift File I/O!"
try? content.write(to: fileURL, atomically: true, encoding: .utf8)
print("Write successful: \(fileURL.lastPathComponent)")
// 2. Read text
if let readContent = try? String(contentsOf: fileURL, encoding: .utf8) {
print("Read content: \(readContent)")
}
// 3. Check if file exists
print("File exists: \(fm.fileExists(atPath: fileURL.path))")
// 4. Get file attributes
if let attrs = try? fm.attributesOfItem(atPath: fileURL.path) {
let size = attrs[.size] as? Int ?? 0
print("File size: \(size) bytes")
}
Output:
TEXT 📖 Display onlyWrite successful: note.txt Read content: Hello, Swift File I/O! File exists: true File size: 21 bytes
4. Codable and JSON
Codable is a protocol combination (Encodable + Decodable) introduced in Swift 4.0 that makes JSON encoding and decoding effortless for custom types.
(1) The Codable Protocol
graph LR
A["Codable"] --> B["Encodable: encode to data"]
A --> C["Decodable: decode from data"]
B --> D["JSONEncoder().encode(obj)"]
C --> E["JSONDecoder().decode(Type.self, from: data)"]
D --> F["Data -> write to file"]
E --> G["Data from file -> typed object"]
| Protocol | Method | Purpose |
|---|---|---|
| Encodable | func encode(to: Encoder) |
Encode an object to Data |
| Decodable | init(from: Decoder) |
Decode from Data to an object |
| Codable | Both combined | Encodable and decodable |
▶ Example: JSON Encoding and Decoding a Custom Object
// ============================================
// Using Codable to JSON encode/decode a Person
// ============================================
import Foundation
struct Person: Codable {
let name: String
let age: Int
let email: String
}
// 1. Encode object to JSON
let alice = Person(name: "Alice", age: 28, email: "alice@example.com")
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let jsonData = try? encoder.encode(alice),
let jsonString = String(data: jsonData, encoding: .utf8) {
print("Encoded JSON:")
print(jsonString)
}
// 2. Decode from JSON to object
let json = """
{"name":"Bob","age":35,"email":"bob@example.com"}
""".data(using: .utf8)!
let decoder = JSONDecoder()
if let bob = try? decoder.decode(Person.self, from: json) {
print("\nDecoded object: \(bob.name), age \(bob.age)")
}
Output:
TEXT 📖 Display onlyEncoded JSON: { "name" : "Alice", "age" : 28, "email" : "alice@example.com" } Decoded object: Bob, age 35
(2) Nested Objects and Custom Keys
Real-world JSON often has nested structures and different key names. Codable handles this with CodingKeys and nested types.
▶ Example: Complex JSON Parsing
// ============================================
// Nested object + custom JSON key name mapping
// ============================================
import Foundation
struct User: Codable {
let id: Int
let fullName: String
let address: Address
let tags: [String]
// Custom key name mapping (JSON snake_case -> Swift camelCase)
enum CodingKeys: String, CodingKey {
case id
case fullName = "full_name"
case address
case tags
}
}
struct Address: Codable {
let city: String
let country: String
}
let json = """
{
"id": 1001,
"full_name": "Charlie Wang",
"address": { "city": "Shanghai", "country": "China" },
"tags": ["developer", "swift"]
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
if let user = try? decoder.decode(User.self, from: json) {
print("User: \(user.fullName) (ID: \(user.id))")
print("City: \(user.address.city)")
print("Tags: \(user.tags.joined(separator: ", "))")
}
Output:
TEXT 📖 Display onlyUser: Charlie Wang (ID: 1001) City: Shanghai Tags: developer, swift
5. UserDefaults and Data Persistence
UserDefaults is suitable for storing small amounts of user preferences. It automatically integrates with iCloud sync and requires no manual file path management.
(1) Storing Data in UserDefaults
| Storage Type | Swift Type | UserDefaults Method |
|---|---|---|
| Integer | Int | set(_:forKey:) + integer(forKey:) |
| Float | Double | set(_:forKey:) + double(forKey:) |
| Boolean | Bool | set(_:forKey:) + bool(forKey:) |
| String | String | set(_:forKey:) + string(forKey:) |
| Array | [Any] | set(_:forKey:) + array(forKey:) |
| Dictionary | [String: Any] | set(_:forKey:) + dictionary(forKey:) |
| Data | Data | set(_:forKey:) + data(forKey:) |
▶ Example: Saving and Reading User Settings
// ============================================
// Saving user preferences with UserDefaults
// ============================================
import Foundation
let defaults = UserDefaults.standard
// 1. Save settings
defaults.set("Alice", forKey: "username")
defaults.set(true, forKey: "isLoggedIn")
defaults.set(3, forKey: "launchCount")
defaults.set(23.5, forKey: "lastTemperature")
// 2. Read settings
let username = defaults.string(forKey: "username") ?? "Guest"
let isLoggedIn = defaults.bool(forKey: "isLoggedIn")
let launchCount = defaults.integer(forKey: "launchCount")
let temperature = defaults.double(forKey: "lastTemperature")
print("User: \(username)")
print("Logged in: \(isLoggedIn)")
print("Launch count: \(launchCount)")
print("Last temperature: \(temperature)°C")
// 3. Remove a key
defaults.removeObject(forKey: "lastTemperature")
print("After removal: \(defaults.double(forKey: "lastTemperature"))")
Output:
TEXT 📖 Display onlyUser: Alice Logged in: true Launch count: 3 Last temperature: 23.5°C After removal: 0.0
(2) Saving Complex Objects to UserDefaults with Codable
UserDefaults cannot directly store Codable objects, but it can store Data -- encode first, then store.
▶ Example: Storing a Custom Type in UserDefaults
// ============================================
// Codable + UserDefaults for storing complex objects
// ============================================
import Foundation
struct Settings: Codable {
var theme: String = "light"
var fontSize: Int = 14
var enableNotifications: Bool = true
}
let defaults = UserDefaults.standard
let encoder = JSONEncoder()
let decoder = JSONDecoder()
// Save
var settings = Settings()
settings.theme = "dark"
settings.fontSize = 16
if let data = try? encoder.encode(settings) {
defaults.set(data, forKey: "appSettings")
print("Settings saved")
}
// Read
if let data = defaults.data(forKey: "appSettings"),
let loaded = try? decoder.decode(Settings.self, from: data) {
print("Theme: \(loaded.theme)")
print("Font size: \(loaded.fontSize)")
print("Notifications: \(loaded.enableNotifications)")
}
Output:
TEXT 📖 Display onlySettings saved Theme: dark Font size: 16 Notifications: true
6. Full Example: To-Do List Manager
// ============================================
// Full example: Persistent to-do list manager
// Features: CRUD + JSON file persistence
// ============================================
import Foundation
// 1. To-do item model
struct TodoItem: Codable {
let id: UUID
var title: String
var isCompleted: Bool
let createdAt: Date
init(title: String) {
self.id = UUID()
self.title = title
self.isCompleted = false
self.createdAt = Date()
}
}
// 2. To-do manager
class TodoManager {
private var items: [TodoItem] = []
private let fileURL: URL
init() {
let docs = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
fileURL = docs.appendingPathComponent("todos.json")
load()
}
func add(title: String) {
items.append(TodoItem(title: title))
save()
}
func toggle(id: UUID) {
if let index = items.firstIndex(where: { $0.id == id }) {
items[index].isCompleted.toggle()
save()
}
}
func showAll() {
for item in items {
let status = item.isCompleted ? "Done" : "Todo"
print("[\(status)] \(item.title)")
}
}
private func save() {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
if let data = try? encoder.encode(items) {
try? data.write(to: fileURL)
}
}
private func load() {
if let data = try? Data(contentsOf: fileURL),
let decoded = try? JSONDecoder().decode([TodoItem].self, from: data) {
items = decoded
}
}
}
// 3. Usage example
let manager = TodoManager()
manager.add(title: "Learn Swift Codable")
manager.add(title: "Write file I/O tutorial")
manager.add(title: "Publish app update")
manager.showAll()
print("--- One completed ---")
// In practice, you'd toggle an item here
Output:
TEXT 📖 Display only[Todo] Learn Swift Codable [Todo] Write file I/O tutorial [Todo] Publish app update --- One completed ---
❓ FAQ
appendingPathComponent, file attribute retrieval, and cross-platform compatibility. String paths are error-prone and lack these features.try? or do-catch. try! crashes the app outright on failure.attrs[.size], attrs[.creationDate], etc.📖 Summary
- FileManager provides the file system interface; common directories are documents, caches, and temporary
- Codable (Encodable + Decodable) enables JSON encoding/decoding for custom types
- JSONEncoder/JSONDecoder support formatted output and custom key name mapping
- UserDefaults is suitable for small preference storage but not for complex or large data
- Complex objects can be encoded to Data first, then stored in UserDefaults
- File operations must handle errors -- disk full, permission denied, etc. are always possible
📝 Exercises
- Basic: Use FileManager to create a "MyNotes" directory under documentDirectory, write a greeting.txt file with the content "Hello, File I/O!", then read and print it.
- Intermediate: Create a Book struct (with title, author, year, isbn), save it to a file using JSONEncoder, then read it back with JSONDecoder and verify data consistency.
- Challenge: Implement a simple KV storage engine
SimpleStoragebased on a JSON file, supportingset(key: String, value: Codable)andget<T>(key: String) -> T?generic methods, with data persisted as a dictionary to file.