【Swift, iOS】UIDocumentBrowserViewControllerでiCloud Driveのファイルを読み込む際にNSCocoaErrorDomain Code=257のエラーが出る問題の対処法

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で検索すると速攻で以下が引っかかったので泣きそうになった

https://stackoverflow.com/questions/46440972/uidocumentpickerviewcontroller-with-uidocumentpickermodeopen-mode-not-giving-per

上記の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で実装するべきだろう

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です