Swift: Swiftファイルとデータ操作:FileManagerとCodable
データの永続化はアプリの中核機能です — ユーザー設定の保存からネットワークデータのキャッシュまで。ファイルI/Oとエンコード/デコードをマスターすることで、アプリ再起動後もデータが保持されます。
1. 学習目標
- FileManagerを使ったファイルの作成、読み取り、書き込み、管理
- Codableプロトコルを使ったカスタム型のJSONエンコード/デコード
- JSONEncoderとJSONDecoderの高度な設定
- UserDefaultsを使ったユーザー設定の保存
- ファイルサンドボックス機構と一般的なディレクトリパスの取得
2. インディー開発者の実話
(1) 課題:アプリ再起動ですべてのデータが消える
Aliceは初めてのiOS Todoアプリをリリースしました。ユーザーは気に入ってくれましたが、致命的な欠陥がありました:アプリを閉じて再度開くと、すべてのTodo項目が消えていました。 ユーザーはApp Storeで1つ星のレビューを残しました。
// Wrong approach: data only lives in memory
var todos: [TodoItem] = [
TodoItem(title: "Buy coffee"),
TodoItem(title: "Write weekly report"),
]
AliceのTodoデータは配列に格納されていました。アプリが閉じられるとメモリが解放され、データは失われました。
(2) Codable + FileManagerによる解決策
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) 利点:ユーザーデータが決して失われない
| 観点 | メモリ内保存 | ファイル永続化 |
|---|---|---|
| アプリ再起動 | データ消失 | データ復元 |
| 保存容量 | RAM制限 | ディスク容量 |
| データ共有 | 非対応 | エクスポート/バックアップ可能 |
| オフライン使用 | 非依存 | 完全オフライン |
| 復元速度 | 即時 | ミリ秒レベル |
3. FileManagerファイル管理
FileManagerはSwiftのファイルシステムインターフェ��スで、ファイル��作成、読み取り、移動、削除などの操作を提供します。
(1) 一般的なディレクトリ
graph TB
A["FileManager.default"] --> B["documentsDirectory"]
A --> C["cachesDirectory"]
A --> D["temporaryDirectory"]
B --> E["ユーザーデー��、iCloudにバックアップ"]
C --> F["キャッシュファイル、システムが削除する可能性あり"]
D --> G["一時ファイル、いつでも削除される"]
| ディレクトリ | 目的 | バックアップ対象? | システム削除? |
|---|---|---|---|
| documentsDirectory | ユーザードキュメントとデータ | はい | いいえ |
| cachesDirectory | キャッシュファイル | いいえ | はい |
| temporaryDirectory | 一時ファイル | いいえ | はい、いつでも |
▶ サンプル: ディレクトリの取得と作成
// ============================================
// 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)")
}
出力:
TEXT 📖 参照専用Documents directory: /Users/alice/Library/Developer/.../Documents Caches directory: /Users/alice/Library/Developer/.../Caches Created directory: /Users/alice/Library/Developer/.../Documents/MyAppData
(2) ファイルの書き込みと読み取り
FileManagerはファイル操作の基本的なメソッドを提供し、Dataのwriteやinit(contentsOf:)と組み合わせてファイルI/Oを行います。
▶ サンプル: テキストファイルの書き込みと読み取り
// ============================================
// 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")
}
出力:
TEXT 📖 参照専用Write successful: note.txt Read content: Hello, Swift File I/O! File exists: true File size: 21 bytes
4. CodableとJSON
CodableはSwift 4.0で導入されたプロトコルの組み合わせ(Encodable + Decodable)で、カスタム型のJSONエンコードとデコードを簡単にします。
(1) Codableプロトコル
graph LR
A["Codable"] --> B["Encodable: データにエンコード"]
A --> C["Decodable: データからデコード"]
B --> D["JSONEncoder().encode(obj)"]
C --> E["JSONDecoder().decode(Type.self, from: data)"]
D --> F["Data -> ファイルに書き込み"]
E --> G["ファイルからのデータ -> 型付きオブジェクト"]
| プロトコル | メソッド | 目的 |
|---|---|---|
| Encodable | func encode(to: Encoder) |
オブジェクトをDataにエンコード |
| Decodable | init(from: Decoder) |
Dataからオブジェクトに��コード |
| Codable | 両方の組み合わせ | エンコードとデコードの両方が可能 |
▶ サンプル: カスタムオブジェクトのJSONエンコードとデコード
// ============================================
// 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)")
}
出力:
TEXT 📖 参照専用エンコードされたJSON: { "name" : "Alice", "age" : 28, "email" : "alice@example.com" } デコードされたオブジェクト: Bob, 年齢 35
(2) ネストオブジェクトとカスタムキー
実際のJSONは多くの場合ネスト構造と異なるキー名を持ちます。CodableはCodingKeysとネスト型でこれに対応します。
▶ サンプル: 複雑なJSON解析
// ============================================
// 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: ", "))")
}
出力:
TEXT 📖 参照専用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:) + bool(forKey:) |
| 文字列 | String | set(_:forKey:) + string(forKey:) |
| 配列 | [Any] | set(_:forKey:) + array(forKey:) |
| 辞書 | [String: Any] | set(_:forKey:) + dictionary(forKey:) |
| Data | Data | set(_:forKey:) + data(forKey:) |
▶ サンプル: ユーザー設定の保存と読み取り
// ============================================
// 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"))")
出力:
TEXT 📖 参照専用User: Alice Logged in: true Launch count: 3 Last temperature: 23.5°C After removal: 0.0
(2) Codableを使った複雑なオブジェクトのUserDefaultsへの保存
UserDefaultsはCodableオブジェクトを直接保存できませんが、Dataは保存できます — まずエンコードしてから保存します。
▶ サンプル: 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)")
}
出力:
TEXT 📖 参照専用Settings saved Theme: dark Font size: 16 Notifications: true
6. 完全な例:Todoリストマネージャー
// ============================================
// 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
出力:
TEXT 📖 参照専用[Todo] Learn Swift Codable [Todo] Write file I/O tutorial [Todo] Publish app update --- One completed ---
❓ よくある質問
appendingPathComponent によるパス連結、ファイル属性の取得、クロスプラットフォーム互換性をサポートし��す。文字列パスはエラーが発生しやすく、これらの機能がありません。try? またはdo-catchを使用してください。try! は失敗時にアプリが完全にクラッシュします。attrs[.size]、attrs[.creationDate] などのキーで値にアクセスします。📖 まとめ
- FileManagerはファイルシステムインターフェースを提供し、一般的なディレクトリはdocuments、caches、temporary
- Codable(Encodable + Decodable)によりカスタム型のJSONエンコード/デコードが可能
- JSONEncoder/JSONDecoderはフォーマット出力とカスタムキー名マッピングをサポート
- UserDefaultsは少量の設定保存に適しているが、複雑なデータや大量データには不向き
- 複雑なオブジェクトはまずDataにエンコードしてからUserDefaultsに保存できる
- ファイル操作はエラー処理が必須 — ディスクフル、権限拒否などの可能性が常にある
📝 練習問題
- 基本: FileManagerを使ってdocumentDirectoryの下に"MyNotes"ディレクトリを作成し、"Hello, File I/O!"という内容のgreeting.txtファイルを書き込み、読み取っ��出力してください。
- 中級: Book構造体(title、author、year、isbnを持つ)を作成し、JSONEncoderでファイルに保存し、JSONDecoderで読み戻してデータの一貫性を確認してください。
- 発展: JSONファイルベースのシンプルなKVストレージエンジン
SimpleStorageを実装し、set(key: String, value: Codable)とget<T>(key: String) -> T?ジェネリックメソッドをサポートし、データを辞書としてファイルに永続化してください。