サンドボックス対応アプリでファイルを選択する方法

App Storeで販売するMacアプリは、2012/06/01からサンドボックス対応が義務化されています。今回ファイルをユーザに選択してもらうアプリを作っていて、このサンドボックスの制限でNSOpenPanelが表示されず困りました。

サンドボックスとは

システム上のファイルを自由に読み書きできないように、アプリ毎にアクセスできる領域を設け、それをサンドボックスと呼び、その中のファイルだけアクセスできるようにする仕組みです。Appleのサンドボックスの仕組みは、アプリがサンドボックス領域以外にアクセスする範囲を明示することでセキュリティを確保します。つまり、アプリのバグや脆弱性をつかれてアプリが想定していないアクセスをされないように、アプリが明示していない部分へはOSがアプリからアクセスできないように制限します。

そのため、ユーザにファイルを選択してもらうには、次のいずれかの方法をとります。

  1. 選択対象のファイルを予めユーザにアプリのサンドボックス内に入れておいてもらう
  2. アプリがサンドボックス外のファイルにもアクセスすることを明示する
  3. NSOpenPanelを使用する

1の方法は一番安全で開発しやすいですが、ユーザにとって使いにくいアプリになるでしょう。

3のNSOpenPanelを使用することは対話的にユーザに許可をもらっていることになるので、サンドボックス外のファイルにアクセスすることができるようになります。また、これを使ってユーザが選択したディレクトリやファイルは、許可済ということで以降アプリの中ではサンドボックス内のファイルと同じ扱いになって自由にアクセスすることができます。

2の方法は、ダウンロード、ミュージック、ムービーなどのディレクトリはXcodeで指定することができるのですが、Xcodeで指定できない領域については、次のようなメソッドを使って範囲を明示してアクセスします。(OS X 10.7.3以降)

bookmarkDataWithOptions:includingResourceValuesForKeys:relativeToURL:error:
URLByResolvingBookmarkData:options:relativeToURL:bookmarkDataIsStale:error:

※詳細はApp Sandbox Design Guideのドキュメントを参照ください。

アプリをサンドボックス対応にする方法

Xcodeのターゲットの設定でEntitlementsにある”App SandBox”を有効にします。

NSOpenPanel,NSSavePanel

そして、NSOpenPanelやNSSavePanelを使う場合は、”User Selected File Access”に”Read Access”もしくは”Read/Write Access”を選択します。

これを初期値の”None”にしていると、NSOpenPanelをコードから表示するようにしても表示されず、10秒程たってからキャンセルボタンが押された結果が返ってきます。コードでエラーを取得する方法はなく、コンソールに次のようなサンドボックス違反のエラーがでます。

私は、最初サンドボックスの制限でNSOpenPanelが開かないというのに気が付かず、システムの負荷が高くてOpenPanelを開くのに時間がかかりすぎているせいかと思い、丸1日悩みました。

その他NSOpenPanelに関すること

NSOpenPanelの使い方

■コード例

 NSOpenPanel *panel = [NSOpenPanel openPanel];

 panel.canChooseFiles = YES; //ファイル選択可能に
 panel.canChooseDirectories = YES;  //ディレクトリ選択可能に
 panel.allowsMultipleSelection = YES;  //複数選択可能に
 panel.resolvesAliases = YES; //エイリアスを解決
 NSArray *types=[[NSArray alloc]initWithObjects:@"txt",@"inf", nil]; 
 panel.allowedFileTypes = types; //選択可能なファイルタイプを指定
 if ([panel runModal] == NSFileHandlingPanelOKButton) { 
   NSArray* urls = [panel URLs];
   //選択されたファイル(ディレクトリ)のURLを処理
 };

※注意点

モーダルシートでNSOpenPanelを表示して、そのcompletionハンドラ内で別のモーダルシートを開くと、NSOpenPanelシートが完全に閉じる前にcompletionハンドラが呼び出されてしまって正常に動作しません。そこで、上記のコード例のように、別ウィンドウとしてNSOpenPanelを表示しています。