
アプリがサンドボックス外(ユーザーの「ファイル」領域や外部ストレージ等)のファイルにアクセスしたいときに重要になるのが startAccessingSecurityScopedResource()
です。
UIDocumentPickerViewController
や SwiftUI の .fileImporter()
などで取得した URL は「セキュリティスコープ付きURL」として渡され、適切にアクセス権を開始・終了する必要があります。
この記事では startAccessingSecurityScopedResource()
の基本的な意味や使い方、主要な引数、注意点までをわかりやすく丁寧に解説します。
startAccessingSecurityScopedResource() とは?
startAccessingSecurityScopedResource()
は、セキュリティスコープ付きURL(security-scoped URL) に対して一時的に読み書きアクセスを許可するためのメソッドです。
iOS / iPadOS / macOS のサンドボックス環境では、アプリは自分のコンテナ外のファイルに自由にアクセスできません。
ユーザー操作で選択されたファイルに限り、アクセス開始(start)とアクセス終了(stop)を明示して扱います。
つまり、「ユーザーが選んだ外部ファイルに、今から触ってもいいですか?」をOSに宣言する処理が startAccessingSecurityScopedResource()
、作業が終わったら stopAccessingSecurityScopedResource()
で終了します。
具体例:.fileImporter() で選んだテキストを安全に読み込む
以下は SwiftUI の .fileImporter()
で選んだ .txt
を、セキュリティスコープを開始してから読み込むサンプルです。
defer
でスコープ終了を保証し、UI更新はメインスレッドで行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
import SwiftUI import UniformTypeIdentifiers struct ContentView: View { @State private var isImporterPresented = false @State private var loadedText = "ファイルを読み込んでください" @State private var didLoad = false var body: some View { VStack { Text(loadedText) .padding() .frame(height: 200) .frame(maxWidth: .infinity) .border(.gray) Button("テキストファイルを読み込む") { isImporterPresented = true } if didLoad { Text("読み込み完了").foregroundColor(.blue) } } .fileImporter( isPresented: $isImporterPresented, allowedContentTypes: [.plainText], allowsMultipleSelection: false ) { result in switch result { case .success(let urls): if let url = urls.first { Task { await loadTextFile(from: url) } } case .failure(let error): print("ファイル選択エラー: \(error.localizedDescription)") } } } private func loadTextFile(from url: URL) async { // アクセス権を開始 let accessed = url.startAccessingSecurityScopedResource() defer { if accessed { url.stopAccessingSecurityScopedResource() } } guard accessed else { await MainActor.run { loadedText = "権限がないため読み込めませんでした" didLoad = false } return } do { let content = try String(contentsOf: url, encoding: .utf8) await MainActor.run { loadedText = content didLoad = true } } catch { await MainActor.run { loadedText = "読み込みに失敗しました: \(error.localizedDescription)" didLoad = false } } } } |
この例では、アクセス開始→読み込み→必ず終了 の順を厳密に守っています。
例外が起きても defer
により終了処理が確実に実行されるのがポイントです。
引数とペアとなるAPI
startAccessingSecurityScopedResource()
は 引数なし のインスタンスメソッドで、返り値 Bool
が重要です。
また、対応するペアAPI として以下を必ず実装する必要があります。
要素 | 説明 |
---|---|
stopAccessingSecurityScopedResource() |
開始したアクセス権を終了する。 開始と終了はカウントで管理されるため、開始回数分、必ず終了を呼ぶ必要がある(対になっていないとリークや予期せぬ拒否の原因)。 |
ブックマークの扱いと更新
セキュリティスコープ付きURLは、一度ユーザーに選んでもらったあとで「ブックマーク」として保存しておくことができます。
これを使うと、次回以降アプリを起動したときにもユーザーに毎回選んでもらう必要がなく、保存済みのファイルに直接アクセスできるようになります。
ただし、このブックマークデータは永遠に有効ではありません。ファイルの場所が変わったり、システムの状態が変化すると古くなることがあります。
そのため、復元時には bookmarkDataIsStale
が true かどうかを必ず確認 し、もし古くなっていたら新しいブックマークデータを作り直して保存する必要があります。
基本の流れは以下の通りです。
- ファイルを選んだときに
bookmarkData(options:includingResourceValuesForKeys:relativeTo:)
を使ってブックマークを保存する - 後日利用するときに
URL(resolvingBookmarkData:options:relativeTo:bookmarkDataIsStale:)
を呼んでURLを復元する - 復元時に
bookmarkDataIsStale
が true なら、更新が必要なので再度bookmarkData(...)
で保存し直す
こうすることで、将来的にも安全に同じファイルへアクセスを続けることができます。
「ブックマークを保存して終わり」ではなく、「復元のたびに新鮮さをチェックして更新する」という考え方が大切です。
利用手順
まず大事なポイントは、アクセスする URL が「セキュリティスコープ付き」になっていること です。
これは「普通のURL」ではなく、「このアプリが使っていいですよ」とOSに認められた特別なURLのことを指します。
セキュリティスコープ付きURLは、主に次のような場面で手に入ります。
- iOS / iPadOS:
UIDocumentPickerViewController
や SwiftUI の.fileImporter()
を使ってユーザーにファイルを選んでもらったときに渡されます。 - macOS:
NSOpenPanel
でファイルを選んだとき、ドラッグ&ドロップで受け取ったとき、または過去に保存しておいた「セキュリティスコープ付きブックマーク」を解決したときに得られます。
実際の流れはとてもシンプルで、次の4ステップです。
- セキュリティスコープ付き
URL
を受け取る let ok = url.startAccessingSecurityScopedResource()
を呼ぶ(アクセスを開始する宣言)ok == true
ならファイルの読み書きを実行する(時間がかかる処理は非同期で行う)- 処理が終わったら必ず
url.stopAccessingSecurityScopedResource()
を呼ぶ(defer
を使うと書き忘れ防止になる)
活用シーン
このメソッドは、アプリが「自分の外」にあるファイルを扱いたいときに欠かせません。
具体的には次のような場面です。
.fileImporter()
やUIDocumentPickerViewController
で選ばれた ユーザーの外部ファイル を読み書きするとき- macOSアプリで
NSOpenPanel
から得たファイルを開き、その場所を 将来も使えるようにブックマーク保存 したいとき - iCloud Drive、USB外部ストレージ、共有フォルダなど、アプリのコンテナ外にあるファイル を扱いたいとき
- 長時間の編集作業をするときに、編集中だけアクセス権を確保して、終わったらきちんと解放したいとき
さらに .fileExporter()
と組み合わせると、「外部から読み込む → 編集する → 別名保存して書き出す」 といったドキュメントワークフローを安全に作ることができます。
注意点
最後に、このメソッドを使うときに特に気をつけたい点をまとめます。
初心者がつまずきやすいところなので、意識しておくと安心です。
- 開始と終了のバランスをとる
start...
を呼んだ回数と同じ回数だけstop...
を呼ばないといけません。止め忘れると不具合の原因になります。defer
を使うと安全です。 - 失敗したときの処理を書く
返り値がfalse
のときはアクセスできません。その場合は「もう一度ファイルを選んでください」とユーザーに促すUIを用意しましょう。 - 長時間アクセスは避ける
開いたままにすると他の動作に悪影響を与える可能性があります。必要なときだけ開き、終わったらすぐ閉じましょう。 - 処理は非同期で行う
大きなファイルを読み書きするときはメインスレッドを止めないようにTask {}
で処理を分けるのがおすすめです。 - ブックマークの更新を忘れない
ブックマークから復元したときにbookmarkDataIsStale
がtrue
なら、情報が古いので更新して再保存が必要です。 - 権限の仕組みを理解する
macOSではApp Sandboxの設定、iOSではユーザー操作を通じた許可が前提になります。「アプリはなんでも勝手に開ける」わけではなく、必ずユーザー操作や権限が必要です。
まとめ
今回は startAccessingSecurityScopedResource()
について詳しく紹介しました。
- ユーザーが選んだ セキュリティスコープ付きURL に対して、一時的にアクセス権を得るためのメソッド
- 返り値
Bool
を確認し、成功時のみファイルI/Oを実行 - 対となる
stopAccessingSecurityScopedResource()
を 必ず 呼ぶ(defer
推奨、開始・終了回数はバランス必須) .fileImporter()
/UIDocumentPickerViewController
/NSOpenPanel
などと組み合わせて使う- 大きなI/Oは非同期処理にし、ブックマーク運用や権限の設計も併せて実装
セキュリティスコープの開始と終了を正しく扱うことで、サンドボックス環境でも安全に外部ファイルを取り込み・活用できます。
ファイルアクセスの基礎として、まずは確実なライフサイクル管理から押さえておきましょう。