iOSアプリを開発していてアプリ外にあるファイルを読み込みたいと思った時にUIDocumentBrowserViewControllerというクラスを見つけた
早速実装してUIDocumentBrowserViewControllerDelegateのdidPickDocumentsAtでiCloud Driveにある選択したファイルのURLをDataにしたところ、シミュレーターでは問題なく動いてホクホク顔をしていたら、実機だとエラーが出て撃沈
まあ結論から言うと、そういう用途の時はUIDocumentPickerViewControllerの方を使うべきで、そちらで探したらすぐ修正方法は見つかったのだが、UIDocumentBrowserViewControllerの方で探すとどこにもなくて無駄に時間を費やしてしまったのでメモ
まずはJSONファイルを読み込む場合を例として以下のように実装する
import UIKit class DataLoadViewController: UIViewController, UIDocumentBrowserViewControllerDelegate { override func viewDidLoad() { super.viewDidLoad() let controller = UIDocumentBrowserViewController.init(forOpeningFilesWithContentTypes: ["public.json"]) controller.delegate = self; controller.allowsDocumentCreation = false; controller.allowsPickingMultipleItems = false; self.present(controller, animated: true, completion: nil) } func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) { controller.dismiss(animated: true, completion: nil) if documentURLs.first == nil { return } let data = try! Data(contentsOf: documentURLs.first!); let jsonStr = String(data: data, encoding: .utf8) print(jsonStr) //jsonStrを使ってあーだーこーだする } }
このViewControllerを呼び出すとファイル操作のできる画面が表示され、そのうちiCloud DriveのJSONファイルを選択するとシミュレーターでは画面が閉じて問題なくJSONのテキストのログが表示されるが、端末だと以下の様なエラーが発生してクラッシュする
・Thread 1: Fatal error: 'try!' expression unexpectedly raised an error: Error Domain=NSCocoaErrorDomain Code=257 "The file “ファイル名.json” couldn’t be opened because you don’t have permission to view it." UserInfo={NSFilePath=/private/var/mobile/Library/Mobile Documents/com~apple~CloudDocs/ファイル名.json, NSUnderlyingError=0x283e1f1b0 {Error Domain=NSPOSIXErrorDomain Code=1 "Operation not permitted"}}
エラー内容を見るにファイルを開く権限がないと言われているようだが、UIDocumentBrowserViewController permissionで検索するとそれらしい情報はいくつかあるもののどれも解決には至らなかった
半分諦めかけていたところ、MacのパーミッションのStack OverflowでNSURLにリソースへのアクセス要求をするメソッドがあるとのことなので、試しに真似して実装してみたら解決した
以下がその実装
func documentBrowser(_ controller: UIDocumentBrowserViewController, didPickDocumentsAt documentURLs: [URL]) { controller.dismiss(animated: true, completion: nil) if documentURLs.first == nil || !documentURLs.first!.startAccessingSecurityScopedResource() { return } let data = try! Data(contentsOf: documentURLs.first!); let jsonStr = String(data: data, encoding: .utf8) //jsonStrを使ってあーだーこーだする documentURLs.first!.stopAccessingSecurityScopedResource() }
startAccessingSecurityScopedResourceでリソースへのアクセス要求し、stopAccessingSecurityScopedResourceでリソースのアクセス要求を取り消している
ちなみに、UIDocumentPickerViewController permissionで検索すると速攻で以下が引っかかったので泣きそうになった
上記のUIDocumentBrowserViewControllerで行っている処理をUIDocumentPickerViewControllerでやると以下のようになる
import UIKit class DataLoadViewController: UIViewController, UIDocumentPickerDelegate { override func viewDidLoad() { super.viewDidLoad() let controller = UIDocumentPickerViewController.init(documentTypes: ["public.json"], in: .open) controller.delegate = self; controller.allowsMultipleSelection = false; self.present(controller, animated: true, completion: nil) } func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { if urls.first == nil || !urls.first!.startAccessingSecurityScopedResource() { return } let data = try! Data(contentsOf: urls.first!); let jsonStr = String(data: data, encoding: .utf8) //jsonStrを使ってあーだーこーだする urls.first!.stopAccessingSecurityScopedResource() } }
こちらであればviewControllerをdismissで閉じる必要もないため、UIDocumentPickerViewControllerで実装するべきだろう