RabbitBoxをドキュメントベースAppにしたい話
前回から時間が経ちましたが、無事にRabbitBoxをDocument-based App化できたので備忘録も兼ねて書こうと思います。リリースから1月ほど経っていますが今のところ安定しています。
今回は具体的な実装方法ではなく、ドキュメントベースAppに関わる予備知識を中心に書いていきます。
apps.apple.comドキュメントベースApp
ドキュメントベースAppと言っても特殊なものではなく、PCのようにアプリデータをドキュメント(≒ファイル)として扱うAppのことです。ただ、iOS/iPad OSのファイルシステムやApple的な慣習によってWindowsやLinuxなどの開発と比べてクセがあります。
iOS / iPad OS App のデータ管理
以前のRabbitBoxもですが、ドキュメントベースAppとしてアプリを開発していない場合、アプリごとに独立したストレージ領域へしかアクセスできません。これをSandboxと呼びます。
このような構造のため、基本的にユーザはPCのようにファイル自体を直接操作する必要がありません。また、各アプリはSandbox外へのアクセスもできないため、セキュリティ的にも安全です。
ドキュメントベースAppのデータ管理
対して、ドキュメントベースAppでは次のようにSandbox外に保存したドキュメント(ファイル)への読み書きを行います。
大した違いはないように感じるかもしれませんが、アプリ内のデータ構造によってはかなり修正が必要になります。
ドキュメントベースAppに必要なデータ構造
ドキュメントベースAppでない場合はアプリ内データを自由に管理できますが、ドキュメントベースAppにする場合はある程度制限がかかります。このため、単純にSandbox外へアクセスできるようにすれば対応できるというものではありませんでした。 RabbitBoxでは次のような対応や検討を行いました。
- ドキュメント仕様の定義
- 1つのドキュメントがどのようなデータを格納するか?やその構造の明確な定義が必要です
- 例えば、サムネイルキャッシュをどうするか?ユーザープリファレンスは格納するか?
- バージョン互換性を保ったまま機能追加できるか?
- ドキュメント単体でデータとして完全か?
- 別デバイスで開いたり、Appを再インストールすると読み込めなくなる仕様ではだめ
- UserDefaults 利用の見直し
- UserDefaultsは非常に便利ですがSandbox内のデータであるためそのままでは利用できません
- 次のような対応が必要です
- ドキュメントのクローズ・オープン時に格納・復元するように実装
- UserDefaultsの利用をやめ、独自実装に変更
- 作業データ管理の検討
- 小さいドキュメントであればドキュメントオープン時にメモリへ読み込めば十分ですが、サイズが大きい場合はSandbox内に作業データ置き場が必要です
- 作業置き場とドキュメントの紐付けはどのように行うか?
ドキュメントの構造
単純なドキュメントの場合、ドキュメント=ファイル として実装できます。しかし、RabbitBoxでは大量の画像ファイルを管理するため、 ドキュメント=パッケージ として実装しています。
パッケージと書くと特殊なOS機能を利用しているかのように感じますが、実態としては単なるディレクトリ(フォルダ)です。ただ、iOS / iPad / Mac上では単一のファイルと同じように扱われます。iCloudでの同期などもパッケージ単位で行われるため単なるディレクトリ構造よりも安全に同期してくれます。
Sandbox外へのアクセス
iOS / iPad OSのアプリは原則Sandbox外へアクセスすることはできません。 (DocumentsディレクトリはSandbox内に含まれるためアクセスできます。)
Security Scoped Resources
AppからSandbox外のファイルへアクセスする場合、ファイル(ディレクトリ)ごとにSandbox内からアクセスできるようにする必要があります。
これはセキュリティスコープ付きURL(Security-Scoped URLs) の URL.startAccessingSecurityScopedResource()
を呼び出すことでアクセスできるようになります。
セキュリティスコープ付きURLは通常のURLと異なりSandbox外へのアクセス許可がついたURLです。このURLは UIDocumentPickerViewController
など一部のAPIを経由してのみ取得できます。これによって、ストレージの/
以下を片っ端からアクセスしたりすることは当然、文字列から作成したURLでもSandbox外部へは自由にアクセスできないようになっています。
NSURL | Apple Developer Documentation
また、上記のアクセス許可はアプリが再起動すると無効になります。 このため、オープンしたドキュメントのURL(ファイルパス)を単純に記憶しておくだけではセキュリティスコープ付きURLを取得することができず、ドキュメントへアクセスできなくなってしまいます。
Security Scoped Bookmark
アプリ内で永続的にSandbox外のリソースへアクセスするためにセキュリティスコープ付きURLをシリアライズ(のようなことを)することができます。これをブックマークと呼びます。ブックマークはパスに依存しない形でリソース(ファイルなど)の位置を記憶するほかセキュリティスコープに関する情報も保持します。
これらを踏まえSandbox外へのアクセスは次のような流れになります。
UIDocumentPickerViewController
でユーザにドキュメントを選択してもらうUIDocumentPickerViewController
の結果としてセキュリティスコープ付きURLを受け取る- セキュリティスコープ付きURLの
startAccessingSecurityScopedResource()
を呼び出す - ファイルへのアクセス
- アクセスが不要になったら
stopAccessingSecurityScopedResource()
で権限を解放する - セキュリティスコープ付きURLから
URL.bookmarkData()
でブックマークを作成し、UserDefaultsなどに格納 - アプリ再起動後、UserDefaultsのブックマークデータからセキュリティスコープ付きURLを生成
- 3へ戻る
ドキュメントの監視、排他制御
ドキュメントベースAppはドキュメントをSandbox外部に保存するため、他のAppやプロセスからアクセスされる可能性があります。具体的にはファイルアプリやiCloudの同期プロセスなどです。
このため、通常のアプリと異なりファイルリソースの監視や排他制御が必要になります。
iOS / iPad OS では File Coordinator と File Presenter という機構を利用してリソース監視と排他制御を実現します。
File Presenterはリソース変更イベントに対する処理を実装したクラスです。File Presenterをファイル(URL)と紐付けてOSに登録すると、各イベントに応じたメソッドが自動で呼び出されるようになります。
このような仕組みでリソースの監視が行えます。どのようなイベントを検知できるかは、ドキュメントから確認できます。
また、File Coordinatorを使用することで複数プロセスから安全にファイルアクセスできます。
NSFileCoordinatorをオブジェクトはcoordinate()メソッドにより、読み書き処理をリクエストすることができ、読み書き可能になったタイミングで非同期にクロージャがコールされます。これにより排他処理が実現できます。
なお、NSFileCoordinatorはFile Presenterと紐付けてインスタンス化します。この紐付けは省略可能なのですが、ドキュメントによると紐付け推奨のようです。主な理由はNSFileCoordinatorによるファイルアクセスのイベントをアプリ自身のFile Presenterが拾ってしまいデッドロックが発生することを回避してくれたりするようです。
おわりに
ざっくりとですが、ドキュメントベースApp対応で調べた内容を記載しました。 長々と書いていますが、実はこれらの実装を隠蔽してくれる UIDocument クラスが提供されており、こちらを使用すると簡単に実装できます。 次回はUIDocumentクラスを用いてドキュメントベースApp対応する方法について記載しようと思います。