Swift: Swift 文件与数据操作:FileManager 与 Codable
数据持久化是 App 的核心能力——从保存用户设置到缓存网络数据,掌握文件 I/O 和编码解码让你的数据在 App 重启后依然存在。
1. 你将学到
- 使用 FileManager 创建、读写和管理文件
- 使用 Codable 协议对自定义类型进行 JSON 编解码
- JSONEncoder 和 JSONDecoder 的高级配置
- UserDefaults 存储用户偏好设置
- 文件沙箱机制和常用目录路径获取
2. 一个独立开发者的真实故事
(1) 痛点:App 重启后数据全丢了
Alice 发布了她第一个 iOS 待办事项 App,用户反馈很好,但有一个致命问题:关闭 App 再打开,所有待办事项都消失了。用户在 App Store 打了一星差评。 `swift // 错误做法:数据只存在内存中 var todos: [TodoItem] = [ TodoItem(title: "买咖啡"), TodoItem(title: "写周报"), ] Alice 的待办数据存在数组里,App 一关闭,内存释放,数据就没了。
(2) Codable + FileManager 的解法
`swift 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) } 下次启动时从文件读取,数据永久保存。
(3) 收益:用户数据不丢失
| 维度 | 内存存储 | 文件持久化 |
|---|---|---|
| App 重启 | ❌ 数据丢失 | ✅ 数据恢复 |
| 存储容量 | RAM 限制 | 磁盘空间 |
| 数据共享 | 不支持 | ✅ 可导出/备份 |
| 离线使用 | 不依赖 | ✅ 完全离线 |
| 恢复速度 | 立即 | ✅ 毫秒级 |
3. FileManager 文件管理
FileManager 是 Swift 的文件系统接口,提供创建、读取、移动、删除文件等操作。
(1) 常用目录
`mermaid graph TB A["FileManager.default"] --> B["documentsDirectory"] A --> C["cachesDirectory"] A --> D["temporaryDirectory"] B --> E["用户数据,会备份到 iCloud"] C --> F["缓存文件,系统可清理"] D --> G["临时文件,随时被删"]
| 目录 | 用途 | 是否备份 | 系统会清理 |
|---|---|---|---|
| documentsDirectory | 用户文档和数据 | ✅ 会备份 | ❌ |
| cachesDirectory | 缓存文件 | ❌ | ✅ |
| emporaryDirectory | 临时文件 | ❌ | ✅ 随时 |
▶ 示例:获取和创建目录
`swift // ============================================ // 获取常用目录路径并创建子目录 // ============================================ import Foundation let fm = FileManager.default // 1. 获取文档目录 let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first! print("文档目录: (docs.path)") // 2. 获取缓存目录 let caches = fm.urls(for: .cachesDirectory, in: .userDomainMask).first! print("缓存目录: (caches.path)") // 3. 创建子目录 let dataDir = docs.appendingPathComponent("MyAppData") if !fm.fileExists(atPath: dataDir.path) { try? fm.createDirectory(at: dataDir, withIntermediateDirectories: true) print("创建目录: (dataDir.path)") }
输出:
ext 文档目录: /Users/alice/Library/Developer/.../Documents 缓存目录: /Users/alice/Library/Developer/.../Caches 创建目录: /Users/alice/Library/Developer/.../Documents/MyAppData
(2) 文件写入与读取
FileManager 提供文件操作的基础方法,配合 Data 的 write 和 init(contentsOf:) 进行文件读写。
▶ 示例:写入和读取文本文件
`swift // ============================================ // 文本文件的写入和读取 // ============================================ import Foundation let fm = FileManager.default let docs = fm.urls(for: .documentDirectory, in: .userDomainMask).first! // 1. 写入文本 let fileURL = docs.appendingPathComponent("note.txt") let content = "Hello, Swift File I/O!" try? content.write(to: fileURL, atomically: true, encoding: .utf8) print("写入成功: (fileURL.lastPathComponent)") // 2. 读取文本 if let readContent = try? String(contentsOf: fileURL, encoding: .utf8) { print("读取内容: (readContent)") } // 3. 检查文件是否存在 print("文件存在: (fm.fileExists(atPath: fileURL.path))") // 4. 获取文件属性 if let attrs = try? fm.attributesOfItem(atPath: fileURL.path) { let size = attrs[.size] as? Int ?? 0 print("文件大小: (size) bytes") }
输出:
ext 写入成功: note.txt 读取内容: Hello, Swift File I/O! 文件存在: true 文件大小: 21 bytes
4. Codable 与 JSON
Codable 是 Swift 4.0 引入的协议组合(Encodable + Decodable),让自定义类型轻松支持 JSON 编解码。
(1) Codable 协议
`mermaid 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"]
| 协议 | 方法 | 作用 |
|---|---|---|
| Encodable | unc encode(to: Encoder) | 将对象编码为 Data |
| Decodable | init(from: Decoder) | 从 Data 解码为对象 |
| Codable | 两者组合 | 可编码也可解码 |
▶ 示例:JSON 编码解码自定义对象
`swift // ============================================ // 使用 Codable 对 Person 进行 JSON 编解码 // ============================================ import Foundation struct Person: Codable { let name: String let age: Int let email: String } // 1. 编码对象为 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("编码后的 JSON:") print(jsonString) } // 2. 从 JSON 解码为对象 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("\n解码后的对象: (bob.name), (bob.age)岁") }
输出: ` ext 编码后的 JSON: { "name" : "Alice", "age" : 28, "email" : "alice@example.com" }
解码后的对象: Bob, 35岁 `
(2) 嵌套对象与自定义键
实际 JSON 通常有嵌套结构和不同的键名,Codable 通过 CodingKeys 和嵌套类型处理。
▶ 示例:复杂 JSON 解析
`swift // ============================================ // 嵌套对象 + 自定义 JSON 键名映射 // ============================================ import Foundation struct User: Codable { let id: Int let fullName: String let address: Address let tags: [String] // 自定义键名映射(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: ", "))") }
输出:
ext User: Charlie Wang (ID: 1001) City: Shanghai Tags: developer, swift
5. UserDefaults 与数据持久化
UserDefaults 适合存储少量用户偏好,自动关联 iCloud 同步,无需手动管理文件路径。
(1) UserDefaults 存储
| 存储类型 | Swift 类型 | UserDefaults 方法 |
|---|---|---|
| 整数 | Int | set(_:forKey:) + integer(forKey:) |
| 浮点数 | Double | set(_:forKey:) + double(forKey:) |
| 布尔值 | Bool | set(_:forKey:) + ool(forKey:) |
| 字符串 | String | set(_:forKey:) + string(forKey:) |
| 数组 | [Any] | set(_:forKey:) + rray(forKey:) |
| 字典 | [String: Any] | set(_:forKey:) + dictionary(forKey:) |
| Data | Data | set(_:forKey:) + data(forKey:) |
▶ 示例:保存和读取用户设置
`swift // ============================================ // UserDefaults 保存用户偏好设置 // ============================================ import Foundation let defaults = UserDefaults.standard // 1. 保存设置 defaults.set("Alice", forKey: "username") defaults.set(true, forKey: "isLoggedIn") defaults.set(3, forKey: "launchCount") defaults.set(23.5, forKey: "lastTemperature") // 2. 读取设置 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("用户: (username)") print("登录: (isLoggedIn)") print("启动次数: (launchCount)") print("上次温度: (temperature)°C") // 3. 删除 key defaults.removeObject(forKey: "lastTemperature") print("删除后读取: (defaults.double(forKey: "lastTemperature"))")
输出:
ext 用户: Alice 登录: true 启动次数: 3 上次温度: 23.5°C 删除后读取: 0.0
(2) 使用 Codable 保存复杂对象到 UserDefaults
UserDefaults 无法直接存储 Codable 对象,但可以存 Data——先编码再存储。
▶ 示例:存储自定义类型到 UserDefaults
`swift // ============================================ // Codable + UserDefaults 存储复杂对象 // ============================================ 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() // 保存 var settings = Settings() settings.theme = "dark" settings.fontSize = 16 if let data = try? encoder.encode(settings) { defaults.set(data, forKey: "appSettings") print("设置已保存") } // 读取 if let data = defaults.data(forKey: "appSettings"), let loaded = try? decoder.decode(Settings.self, from: data) { print("主题: (loaded.theme)") print("字号: (loaded.fontSize)") print("通知: (loaded.enableNotifications)") }
输出:
ext 设置已保存 主题: dark 字号: 16 通知: true
6. 完整示例:待办事项管理器
`swift // ============================================ // 完整示例:待办事项持久化管理器 // 功能:增删改查 + JSON 文件持久化 // ============================================ import Foundation // 1. 待办事项模型 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. 待办管理器 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: { .id == id }) { items[index].isCompleted.toggle() save() } } func showAll() { for item in items { let status = item.isCompleted ? "✅" : "⬜" 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. 使用示例 let manager = TodoManager() manager.add(title: "学习 Swift Codable") manager.add(title: "写文件操作教程") manager.add(title: "发布 App 更新") manager.showAll() print("--- 已完成一项 ---") // 实际中这里会 toggle 某项
输出:
ext ⬜ 学习 Swift Codable ⬜ 写文件操作教程 ⬜ 发布 App 更新 --- 已完成一项 ---
❓ 常见问题
📖 小节
- FileManager 提供文件系统操作接口,常用目录有 documents、caches、temporary
- Codable(Encodable + Decodable)让自定义类型支持 JSON 编解码
- JSONEncoder/JSONDecoder 支持格式化输出、自定义键名映射
- UserDefaults 适合存储小型偏好设置,但不适合复杂或大量数据
- 复杂对象可先编码为 Data 再存入 UserDefaults
- 文件操作必须处理错误——磁盘满、权限不足等总是可能发生
📝 作业
- 基础题: 使用 FileManager 在 documentDirectory 下创建 "MyNotes" 目录,写入一个 greeting.txt 文件,内容为 "Hello, File I/O!",然后读取并打印。
- 进阶题: 创建一个 Book 结构体(含 title、author、year、isbn),使用 JSONEncoder 保存到文件,再读取并用 JSONDecoder 恢复,验证数据一致性。
- 挑战题: 实现一个简单的 KV 存储引擎 SimpleStorage,基于 JSON 文件,支持 set(key: String, value: Codable) 和 get(key: String) -> T? 泛型方法,数据以字典形式持久化到文件。