Swift: Swiftファイルとデータ操作:FileManagerとCodable

データの永続化はアプリの中核機能です — ユーザー設定の保存からネットワークデータのキャッシュまで。ファイルI/Oとエンコード/デコードをマスターすることで、アプリ再起動後もデータが保持されます。

1. 学習目標


2. インディー開発者の実話

(1) 課題:アプリ再起動ですべてのデータが消える

Aliceは初めてのiOS Todoアプリをリリースしました。ユーザーは気に入ってくれましたが、致命的な欠陥がありました:アプリを閉じて再度開くと、すべてのTodo項目が消えていました。 ユーザーはApp Storeで1つ星のレビューを残しました。

SWIFT
// Wrong approach: data only lives in memory
var todos: [TodoItem] = [
    TodoItem(title: "Buy coffee"),
    TodoItem(title: "Write weekly report"),
]

AliceのTodoデータは配列に格納されていました。アプリが閉じられるとメモリが解放され、データは失われました。

(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) 利点:ユーザーデータが決して失われない

観点 メモリ内保存 ファイル永続化
アプリ再起動 データ消失 データ復元
保存容量 RAM制限 ディスク容量
データ共有 非対応 エクスポート/バックアップ可能
オフライン使用 非依存 完全オフライン
復元速度 即時 ミリ秒レベル

3. FileManagerファイル管理

FileManagerはSwiftのファイルシステムインターフェ��スで、ファイル��作成、読み取り、移動、削除などの操作を提供します。

(1) 一般的なディレクトリ

100%
graph TB
    A["FileManager.default"] --> B["documentsDirectory"]
    A --> C["cachesDirectory"]
    A --> D["temporaryDirectory"]
    B --> E["ユーザーデー��、iCloudにバックアップ"]
    C --> F["キャッシュファイル、システムが削除する可能性あり"]
    D --> G["一時ファイル、いつでも削除される"]
ディレクトリ 目的 バックアップ対象? システム削除?
documentsDirectory ユーザードキュメントとデータ はい いいえ
cachesDirectory キャッシュファイル いいえ はい
temporaryDirectory 一時ファイル いいえ はい、いつでも

▶ サンプル: ディレクトリの取得と作成

SWIFT
// ============================================
// 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を行います。

▶ サンプル: テキストファイルの書き込みと読み取り

SWIFT
// ============================================
// 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プロトコル

100%
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エンコードとデコード

SWIFT
// ============================================
// 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解析

SWIFT
// ============================================
// 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:)

▶ サンプル: ユーザー設定の保存と読み取り

SWIFT
// ============================================
// 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へのカスタム型の保存

SWIFT
// ============================================
// 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リストマネージャー

SWIFT
// ============================================
// 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 ---

❓ よくある質問

Q UserDefaultsとファイル保存の選び方は?
A UserDefaultsは100KB未満の単純なキーバリューペア(ユーザー設定、トークン)に適しています。構造化データや100KBを超えるデータにはファイル保存(JSON/データベース)を使用します。UserDefaultsはパフォーマンスが悪く、部分更新をサポートしていません。
Q JSONEncoderの出力形式をどう制御しますか?
A encoder.outputFormattingを .prettyPrinted(人間が読める)、.sortedKeys(安定した順序)、.withoutEscapingSlashes(エスケープ削減)に設定します。
Q ファイルパスにはStringとURLのどちらを使うべきですか?
A 常にURLを使用してください。URLは appendingPathComponent によるパス連結、ファイル属性の取得、クロスプラットフォーム互換性をサポートし��す。文字列パスはエラーが発生しやすく、これらの機能がありません。
Q ファイル操作でtry?とtry!はどう使うべきですか?
A ファイル操作は失敗の確率が高いです(ディスクフル、権限拒否、ファイ��が見つからない)。常に try? またはdo-catchを使用してください。try! は失敗時にアプリが完全にクラッシュします。
Q FileManagerのattributesOfItemはどんな情報を提供しますか?
A ファイルサイズ、作成日、変更日、権限、所有者、ディレクトリかどうかなどです。attrs[.size]attrs[.creationDate] などのキーで値にアクセスします。
Q iOSのサンドボックス機構とは何ですか?
A 各アプリは自身のサンドボックスディレクトリ(Documents、Caches、tmp)にのみアクセスでき、他のアプリのファイルシステムを読み書きできません。FileManager経由で取得したURLはサンドボックス範囲内です。

📖 まとめ


📝 練習問題

  1. 基本: FileManagerを使ってdocumentDirectoryの下に"MyNotes"ディレクトリを作成し、"Hello, File I/O!"という内容のgreeting.txtファイルを書き込み、読み取っ��出力してください。
  2. 中級: Book構造体(title、author、year、isbnを持つ)を作成し、JSONEncoderでファイルに保存し、JSONDecoderで読み戻してデータの一貫性を確認してください。
  3. 発展: JSONファイルベースのシンプルなKVストレージエンジン SimpleStorage を実装し、set(key: String, value: Codable)get<T>(key: String) -> T? ジェネリックメソッドをサポートし、データを辞書としてファイルに永続化してください。
Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%