[Xcode/Swift] iOS FileManager と URL設計思想 ①

URL-based API の正体と、Apple がそれを推す理由

iOS開発をやっていると、こんな疑問にぶつかることがある。

// こっちも動く
let path = "/tmp/sample.txt"

// でも最近の Apple API はこっちを要求してくる
let url = URL(fileURLWithPath: "/tmp/sample.txt")

どっちでも同じじゃないのかと思うが、Apple の API を使い続けるうちに気づく。FileManager も、URLSession も、AVFoundation も、最近の API は ほぼ全部 URL を要求してくる

この記事ではその設計思想の根っこから丁寧に掘り下げて、単なる「FileManager の使い方」ではなく、「なぜそう設計されているのか」 を理解できるように情報をまとめていきます。

String path の時代

Objective-C 時代、ファイルパスは普通の文字列で扱っていました。

// Objective-C 時代の書き方
NSString *path = @"/Users/taro/Documents/sample.txt";
NSFileManager *fm = [NSFileManager defaultManager];
BOOL exists = [fm fileExistsAtPath:path];

これ自体は動くけど、ここ問題で問題が出てきます。

それは、文字列はただの文字列ということ。

@"/Users/taro/Documents/sample.txt" という文字列に、OS はなんの意味も持たせておらずそれが本当にファイルシステム上のパスなのか、ただのテキストなのか、コンパイラは判断できないという状態になります。

String path の問題点

1. パス連結のミス

let dir = "/Users/taro/Documents"
let file = "sample.txt"

// 手動連結すると...
let path = dir + "/" + file  // まだマシ
let path2 = dir + file       // "/Users/taro/Documentssample.txt" になってしまう

スラッシュの付け忘れは古典的なバグとなり、特にパスを動的に組み立てるときに起きやすいです。

2. エンコーディングの問題

ファイル名に日本語やスペースが入ったとき、String ではエンコーディングを意識しないといけない。URL ならそれが構造として扱われてしまう。

3. 型情報がない

String 型はファイルパスを表す型ではないため関数のシグネチャに path: String と書かれていても、それがファイルパスなのか、URL 文字列なのか、ただのキーなのかが型だけでは判断できない。

4. ファイルシステムの抽象化ができない

ネットワーク上のリソース、iCloud Drive 上のファイル、セキュリティスコープ付きのファイルなど、現代の iOS 環境ではファイルの「場所」は単純なパス文字列では表現しきれない。

URL とは何か? Web だけじゃない

URL というと多くの人は https://example.com のような Web URL を思い浮かべますが、 URL(Uniform Resource Locator)の “Resource” はもっと広い概念として捉えられます。

基本のURL構造:

scheme://host/path?query#fragment

たとえば:

https://example.com/api/users?id=1
file:///Users/taro/Documents/sample.txt # file:// スキームは、ローカルファイルシステム上のリソースを指す URL

// file:// URL の作り方
let url = URL(fileURLWithPath: "/Users/taro/Documents/sample.txt")
print(url)  // file:///Users/taro/Documents/sample.txt
print(url.isFileURL)  // true

Apple が「URL でファイルを扱う」というのは、ファイルシステム上のリソースも Web リソースと同じ抽象で扱える という設計思想ということです。

appendingPathComponent の安全性

URL を使うと、パス連結が安全に扱えるようになります。

let documentsURL = FileManager.default.urls(
    for: .documentDirectory,
    in: .userDomainMask
).first!

// URL ならパス連結が安全
let fileURL = documentsURL.appendingPathComponent("sample.txt")
// file:///Users/taro/.../Documents/sample.txt

// さらにディレクトリを追加
let subDirURL = documentsURL
    .appendingPathComponent("Images", isDirectory: true)
    .appendingPathComponent("profile.jpg")
// file:///Users/taro/.../Documents/Images/profile.jpg

appendingPathComponent は自動的にスラッシュを処理してくれるため、手動で文字列連結する必要がなくなります。

isDirectory: true を付けることでディレクトリであることを明示できて、これはファイルシステムの意味論が URL の構造に組み込まれていることを示しています。

また iOS 16 以降では .appending(path:) という新しい API も追加されました。

// iOS 16+
let fileURL = documentsURL.appending(path: "sample.txt")
let dirURL = documentsURL.appending(path: "Images/", directoryHint: .isDirectory)

iOS Sandbox と URL の関係

iOS には Sandbox という仕組みがあり、アプリは自分に割り当てられた領域(Sandbox)の中でしかファイルを読み書きできないようになっています。

<アプリコンテナ>/
  ├── Documents/      ← ユーザーデータ。iCloud バックアップ対象
  ├── Library/
  │   ├── Caches/     ← キャッシュ。バックアップ対象外
  │   └── Application Support/
  ├── tmp/            ← 一時ファイル。再起動で削除される
  └── <バイナリ>

String path でハードコードすることの危険性がまさにここにありという感じです。

// ❌ ハードコードはダメ
let bad = "/var/mobile/Containers/Data/Application/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX/Documents"

// ✅ FileManager 経由で取得する
let good = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!

Sandbox のパスはアプリのインストールごとに変わる(UUID 部分)ため、ハードコードしたパスはリリース後に動かなくなる可能性があります。

FileManager の URL 取得 API を使えば、常に正しいパスが返ってくるので、これが URL ベース API の大きな利点となります。

FileManager:URL 中心の設計

FileManager の古い API と新しい API :

// 古い String ベース API(今も使えるが deprecated 気味)
func fileExists(atPath path: String) -> Bool
func createDirectory(atPath path: String, ...) throws
func contentsOfDirectory(atPath path: String) throws -> [String]

// 新しい URL ベース API
func fileExists(at url: URL) -> Bool  // iOS 16+
func createDirectory(at url: URL, ...) throws
func contentsOfDirectory(at url: URL, ...) throws -> [URL]

特に注目したいのが contentsOfDirectory(at:)部分:

let documentsURL = FileManager.default.urls(
    for: .documentDirectory,
    in: .userDomainMask
).first!

// String ベース(古い)
let fileNames = try FileManager.default.contentsOfDirectory(atPath: documentsURL.path)
// → [String] が返ってくる。フルパスではなくファイル名だけ

// URL ベース(新しい)
let fileURLs = try FileManager.default.contentsOfDirectory(
    at: documentsURL,
    includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey],
    options: [.skipsHiddenFiles]
)
// → [URL] が返ってくる。完全な URL で使いやすい

contentsOfDirectory(at:) には includingPropertiesForKeys というパラメータがあり、これが次のセクションで解説する URLResourceValues に関係してくる部分となります。

temporaryDirectory:一時ファイルの扱い方

ZIP 解凍などの処理では、一時ディレクトリをよく使う機会があります。

// iOS 10 以降
let tmpURL = FileManager.default.temporaryDirectory
// file:///private/var/mobile/.../tmp/

// 一時ファイルの URL を作る
// UUID().uuidString を使うことで、複数の処理が同時に走っても衝突しない一時ディレクトリが作れる。
let workURL = tmpURL.appendingPathComponent(UUID().uuidString, isDirectory: true)

// ディレクトリを作成
try FileManager.default.createDirectory(at: workURL, withIntermediateDirectories: true)

// 処理が終わったら削除
// removeItem(at:) も URL を受け取る。古い removeItem(atPath:) ではなく URL 版を使うのが modern な書き方
defer {
    try? FileManager.default.removeItem(at: workURL)
}

まとめ

  • iOS Sandbox の仕組み上、ハードコードした String path は危険
  • String path は型情報がなく、連結ミスや Sandbox 外アクセスのリスクがある
  • URL はファイルシステムリソースを抽象化した型で、file:// スキームを持つ
  • appendingPathComponent で安全にパスを組み立てられる
  • FileManager は URL 中心の新 API に移行している

まずはこのあたりの基礎を理解出るようになればOKです。

次の記事でSSZipArchiveやZIPFoundationも含めた具体的な話をしていきます。

[Xcode/Swift] iOS FileManager と URL設計思想 ②