チームがクラウドMacからiOSアプリをデバッグするとき、「ファイル」でPDFを選んでプレビューできれば、取り込みは完了したと判断しがちです。しかし、アプリが外部ファイルを一時的に読めているだけかもしれません。次回起動時に元のパスへアクセスできなくなれば、読書記録からファイルを開けなくなります。検証すべきなのは、ファイル選択画面が表示されるかどうかではなく、外部プロバイダから受け取ったファイルのアクセス権、所有先、ライフサイクルです。
この記事では、PDFを取り込んで長期間読むケースを扱います。開発用Macにはリモート接続できますが、ファイルアクセスの規則を決めるのはアプリが動作するiOSデバイスです。まず実装と自動テストを分けて考えます。実装では実際のファイル選択画面から渡されるURLを扱い、テストではアプリが所有するコピーと失敗時の動作を検証します。
取り込み完了の条件を先に決める
ファイルを開けることと、そのファイルをアプリが所有することは別です。UIDocumentPickerViewController(forOpeningContentTypes: [.pdf], asCopy: false) を使う場合、コールバックで受け取るURLはファイル選択画面が提供した場所を指します。永続的なパスとして読書記録に直接保存してはいけません。次回も開けることが要件なら、アクセスが有効な間に元ファイルを読み、アプリ自身の永続ディレクトリへコピーして、記録には新しいコピーを参照させます。
まず、観測できる結果を三つ決めます。選択をキャンセルしても記録は増えないこと、コピーに失敗しても「成功」した記録は増えないこと、成功後に元ファイルを移動してもアプリの再起動後に自身のコピーを開けることです。一時ディレクトリは一回の取り込み処理の作業場所には適していますが、長期保存する書庫には向きません。記録を長期保存するなら、検証済みの一時コピーをApplication Supportなどのアプリの永続ディレクトリへ移します。
取り込み成功と判断するのは、ファイル選択画面からURLが返った瞬間ではなく、コピーを書き込んで検証した後です。
アクセスが有効な間に協調読み取りとコピーを済ませる
ファイル選択画面のコールバックでURLを受け取ったら、まずセキュリティスコープ付きリソースへのアクセスを開始し、NSFileCoordinator で読み取りを調整します。次の関数が示すのは、単一の小さなPDFを一時保存する手順だけです。内容の検証、永続ディレクトリへの移動、記録の更新方法は呼び出し側で決める必要があります。この例は asCopy: false の開くモードを前提としており、あらゆる入力元を外部のアクセス許可が必要なファイルとみなす汎用コピー関数ではありません。
func stagePickedPDF(_ url: URL) throws -> URL {
guard url.startAccessingSecurityScopedResource() else {
throw CocoaError(.fileReadNoPermission)
}
defer { url.stopAccessingSecurityScopedResource() }
let destination = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("pdf")
var coordinationError: NSError?
var copyError: Error?
NSFileCoordinator().coordinate(
readingItemAt: url,
options: [],
error: &coordinationError
) { readableURL in
do {
try FileManager.default.copyItem(
at: readableURL,
to: destination
)
} catch {
copyError = error
}
}
if let coordinationError { throw coordinationError }
if let copyError { throw copyError }
return destination
}
coordinate は同期呼び出しです。大きなファイルのコピーでUIスレッドを長時間ふさがないようにしてください。実装では「アクセス開始―協調読み取り―アクセス終了」の全体をバックグラウンドタスクで実行できます。コールバック内で先にアクセスを終了し、外部URLだけをバックグラウンドタスクに渡してはいけません。エラー時にもアクセスを終了する必要があり、ここでは終了処理を各所に書くより defer のほうが確認しやすくなります。
一時ファイルを復元可能な記録にする
コピー後は、ファイルの存在と妥当なサイズを確認し、PDFパーサーで開けるか試します。拡張子が .pdf であるだけでは内容の正しさは証明できません。検証に成功してからアプリの永続ディレクトリへ移動し、アプリが生成した識別子に紐づけて相対パスを保存します。検証や移動に失敗した場合は一時ファイルを削除し、再試行できるエラーをユーザーに表示して、読書リストには追加しません。
永続化では、「ファイルは書けたが、記録の書き込みに失敗した」という途中の失敗も考慮します。先に保留中の記録を作り、移動が完了してから利用可能とマークする方法があります。起動時には未完了の記録を調べ、対応するファイルを確認して残留物を整理します。具体的なトランザクションの組み方はアプリのデータ保存方式によりますが、目的は同じです。存在しないファイルを指す項目をリストに残さないことです。
ファイル選択画面から渡された表示名を、そのまま保存先のパスに連結しないでください。アプリが生成したファイル名を使い、元の名前は表示用のフィールドとして保存します。これなら同名ファイルによる上書きを避け、パスに使いにくい文字による問題も減らせます。コピーを保持せず、常に外部の元ファイルを読むことが製品要件なら、それは別のアクセス契約です。再認可や参照元を利用できない場合の画面を別途設計する必要があり、「ローカルに取り込み済み」という成功状態を流用してはいけません。
クラウドMacから段階的に検証する
まずXCTestで、アプリのサンドボックス内にあるPDFサンプルを取り込み後の処理に渡します。解析の成功、破損ファイルのエラー、同名ファイルで上書きされないこと、一時ファイルを削除してもリストが永続コピーを参照することを確認します。このテストでセキュリティスコープの認可まで検証したと主張してはいけません。サンドボックス内のサンプルには通常、実際のファイルプロバイダ経由のアクセス条件がありません。
次にシミュレータで画面操作を一通り試します。「ファイル」の選択画面から見えるPDFを用意し、選択とキャンセルをそれぞれ実行します。取り込み後にアプリを終了して再起動し、読書記録から開けることを確認します。さらに、選択画面に表示されていた元ファイルを移動し、アプリが本当に自身のコピーを読んでいるか調べます。自動化で安定して操作できないプロバイダの挙動は手動検証項目として記録し、固定時間の待機で不確実さを隠さないでください。
最後に、対象デバイスと実際に利用するファイルプロバイダで、アクセス許可とダウンロード時の挙動を再確認します。選択時点では、まだローカルへの保存が完了していない書類もあります。読み取りに失敗したら、画面にエラーを残して再選択を可能にし、空のコピーを作らないようにします。ここで検証するのはエラー処理であり、プロバイダのダウンロード所要時間を保証することではありません。
失敗ケースの一覧で締めくくる
| 操作 | 期待する結果 |
|---|---|
| 選択をキャンセル | 読書リストは変わらず、一時ファイルも残らない |
| 元ファイルを読み取れない | エラーを明示し、成功した記録を作らない |
| コピーの検証に失敗 | 一時ファイルを削除し、再試行できるようにする |
| 取り込み後に元ファイルを移動 | 再起動後もアプリのコピーを開ける |
| 取り込み中にアプリが中断 | 次回起動時に未完了の記録を検出して処理できる |
検証記録には、使用したファイル形式、参照元の種類、OSとアプリのビルドバージョン、各操作の実際の結果を添えるとよいでしょう。リモートデスクトップで問題を再現するときも、同じファイルを何度も選び直す前に、失敗箇所がアクセス許可、協調読み取り、コピー、検証、記録の確定のどこかを切り分けられます。
よくある質問
書類ピッカーから受け取ったパスを保存して、次回も使えますか?
外部のパスを永続的な参照として扱わないでください。継続利用が必要ならアクセス可能な間に協調読み取りを行い、アプリの永続領域へコピーします。
シミュレータだけで書類取り込みを検証できますか?
十分ではありません。コピーや解析、エラー処理は確認できますが、権限やキャンセル、提供元のファイル移動は実際の書類ピッカーでも確かめます。
リモート接続できる専有物理Macをお探しですか?
M4 Start、M4 Work、M4 Proのスペック、ロケーション、課金期間を比較して、用途に合ったプランをお選びください。