web-dev-qa-db-ja.com

UIDocumentPickerViewControllerは、存在しないファイルにURLを返します

私は UIDocumentPickerViewController を使用して、ユーザーがバックエンドにアップロードするためにiCloudドライブからファイルを選択できるようにしています。

ほとんどの場合、正しく機能します。ただし、場合によっては(特にインターネット接続が不安定な場合) documentPicker:didPickDocumentAtURL: は、ファイルシステムに実際には存在しないURLを提供し、それを使用しようとすると、NSError「そのようなファイルまたはディレクトリはありません」が返されます。

これを処理する正しい方法は何ですか? NSFileManager fileExistsAtPath: 存在しない場合は、再試行するようにユーザーに指示します。しかし、それはあまりユーザーフレンドリーに聞こえません。 iCloudドライブから本当のエラー理由を取得し、おそらくiCloudドライブに再試行するように指示する方法はありますか?

コードの関連部分:

@IBAction func add(sender: UIBarButtonItem) {
    let documentMenu = UIDocumentMenuViewController(
        documentTypes: [kUTTypeImage as String],
        inMode: .Import)

    documentMenu.delegate = self
    documentMenu.popoverPresentationController?.barButtonItem = sender
    presentViewController(documentMenu, animated: true, completion: nil)
}

func documentMenu(documentMenu: UIDocumentMenuViewController, didPickDocumentPicker documentPicker: UIDocumentPickerViewController) {
    documentPicker.delegate = self
    documentPicker.popoverPresentationController?.sourceView = self.view
    presentViewController(documentPicker, animated: true, completion: nil)
}

func documentPicker(controller: UIDocumentPickerViewController, didPickDocumentAtURL url: NSURL) {
    print("original URL", url)

    url.startAccessingSecurityScopedResource()

    var error: NSError?
    NSFileCoordinator().coordinateReadingItemAtURL(
    url, options: .ForUploading, error: &error) { url in
        print("coordinated URL", url)
    }

    if let error = error {
        print(error)
    }

    url.stopAccessingSecurityScopedResource()
}

OS XのiCloudドライブに2つの大きな画像(それぞれ約5MiB)を追加し、そのうちの1つだけを開くことでこれを再現しました(a synced file.bmp)iPhoneで、もう一方を開かない(an unsynced file.bmp)。そして、WiFiをオフにしました。次に、アプリケーションでそれらを選択しようとしました。

同期されたファイル:

original URL file:///private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/example.com.demo-Inbox/a%20synced%20file.bmp
coordinated URL file:///private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/CoordinatedZipFileDR7e5I/a%20synced%20file.bmp

同期されていないファイル:

original URL file:///private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/example.com.demo-Inbox/an%20unsynced%20file.bmp
Error Domain=NSCocoaErrorDomain Code=260 "The file “an unsynced file.bmp” couldn’t be opened because there is no such file." UserInfo={NSURL=file:///private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/example.com.demo-Inbox/an%20unsynced%20file.bmp, NSFilePath=/private/var/mobile/Containers/Data/Application/CE70EE57-B906-4BF8-B351-A57110BE2B01/tmp/example.com.demo-Inbox/an unsynced file.bmp, NSUnderlyingError=0x15fee1210 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
12
imgx64

説明

同様の問題が私にも起こりました。ドキュメントピッカーを次のように初期化しました。

_var documentPicker: UIDocumentPickerViewController = UIDocumentPickerViewController(documentTypes: ["public.data"], in: .import)
_

つまり、ファイルはdocumentPickerで選択された後、_app_id-Inbox_ディレクトリにコピーされます。デリゲートメソッドdocumentPicker(_:didPickDocumentsAt:)が呼び出されると、_app_id-Inbox_ディレクトリにあるファイルを指すURLが提供されます。

問題

しばらくすると(アプリを閉じずに)、これらのURLは存在しないファイルを指していました。これは、_app_id-Inbox_フォルダー内の_tmp/_がその間にクリアされたために発生しました。たとえば、ドキュメントを選択してテーブルビューに表示し、iPhoneをその画面に1分ほど置いたままにします。次に、QLPreviewControllerから提供されたURLを使用してdocumentPickerでファイルを開く特定のドキュメントをクリックしようとすると、存在しないファイルが返されます。

Appleのドキュメントには次のように記載されているため、これはバグのようです ここ

UIDocumentPickerModeImport

URLは、選択したドキュメントのコピーを参照します。これらのドキュメントは一時ファイルです。これらは、アプリケーションが終了するまでのみ使用できます。永続的なコピーを保持するには、これらのファイルをサンドボックス内の永続的な場所に移動します。

アプリケーションが終了するまでとはっきりと書かれていますが、私の場合は、そのURLを開かなかったのは約1分でした。

Workaround

ファイルを_app_id-Inbox_フォルダーから_tmp/_またはその他のディレクトリに移動し、新しい場所を指すURLを使用します。

Swift 4

_func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
    let newUrls = urls.flatMap { (url: URL) -> URL? in
        // Create file URL to temporary folder
        var tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
        // Apend filename (name+extension) to URL
        tempURL.appendPathComponent(url.lastPathComponent)
        do {
            // If file with same name exists remove it (replace file with new one)
            if FileManager.default.fileExists(atPath: tempURL.path) {
                try FileManager.default.removeItem(atPath: tempURL.path)
            }
            // Move file from app_id-Inbox to tmp/filename
            try FileManager.default.moveItem(atPath: url.path, toPath: tempURL.path)
            return tempURL
        } catch {
            print(error.localizedDescription)
            return nil
        }
    }
    // ... do something with URLs
}
_

システムは_/tmp_ディレクトリを処理しますが、不要になったときにコンテンツをクリアすることをお勧めします。

25
Najdan Tomić

ドキュメントピッカーを使用している間、ファイルシステムを使用するには、以下のようにURLをファイルパスに変換します。

var filePath = urls[0].absoluteString
filePath = filePath.replacingOccurrences(of: "file:/", with: "")//making url to file path
2
Utkarsh Goel

問題は、tmpディレクトリが何らかの方法でクリーンアップされることではないと思います。

シミュレーターを使用してicloudからファイルを開くと、ファイルがapp-id-Inboxに存在し、クリーンアップ/削除されていないことがわかります。したがって、ファイルを読み取ろうとしたり、コピーしてファイルが存在しないというエラーが発生したりすると、ファイルがまだ存在していることがわかるため、セキュリティ上の問題だと思います。

DocumentPickerViewControllerのインポートモードで、これで解決します(申し訳ありませんが、c#コードを貼り付けますが、Swiftは目の前にあるため)

私が行ったNSUrlを返すDidPickDocumentメソッド内

NSData fileData = NSData.FromUrl(url);

これで、ファイルデータを含む「fileData」変数ができました。

次に、アプリの隔離されたスペースにある自分のカスタムフォルダーにそれらをコピーすると、問題なく動作します。

2
Peter Bournakas

同じ問題が発生しましたが、表示されている一時ディレクトリを削除していたことが判明しました。したがって、表示されたときに保存してから削除し、documentPicker:didPickDocumentAtURLを適切に呼び出します。削除したファイルを指すのはURLだけです。

1
Jesper

ドキュメントピッカーを使用している間、ファイルシステムを使用するには、以下のようにURLをファイルパスに変換します。

var filePath = urls[0].absoluteString
filePath = filePath.replacingOccurrences(of: "file:/", with: "")//making url to file path
0
Utkarsh Goel