imgx64
imgx64

Reputation: 4152

UIDocumentPickerViewController returns url to a file that does not exist

I'm using UIDocumentPickerViewController to let the user select a file from iCloud Drive for uploading to the backend.

Most of the time, it works correctly. However, sometimes (especially when the internet connection is spotty)documentPicker:didPickDocumentAtURL: gives a url that does not actually exist on the filesystem, and any attempt to use it returns a NSError "No such file or directory".

What is the correct way to handle this? I'm thinking about using NSFileManager fileExistsAtPath: and tell the user to try again if it doesn't exist. But that doesn't sound very user friendly. Is there a way to get the real error reason from iCloud Drive and perhaps tell iCloud Drive to try again?

The relevant parts of the code:

@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()
}

I reproduced this by adding two large images (~5MiB each) to iCloud Drive on OS X and opening only one of them (a synced file.bmp) on an iPhone and not opening the other (an unsynced file.bmp). And then turned off WiFi. Then I tried to select them in my application:

The synced file:

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

The unsynced file:

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"}}

Upvotes: 26

Views: 22191

Answers (7)

user1251560
user1251560

Reputation: 119

I can add something interesting to this.

This problem also happens when the same file is imported repeatedly. I ran into this issue with a very small file (~900 bytes) on the local file system.

iOS will delete the file exactly 60 seconds after placing it in the inbox and having notified the delegate. It appears these deletion operations can get queued up and start wreaking havoc after 60 seconds if you import a file again with the same name (or last path component). This happens both on iOS 13 (deprecated initializer) and 14 (new initializer).

I tried a well-tested code snippet to perform a coordinated delete, but it didn't help.

This issue is very much alive and is a huge detriment to the import feature; I don't think the use case is extremely rare or outlandish. Although there aren't many posts about this apparently - maybe everyone just switched to open from import (even if it mandates coordinated file operations if you want to be good citizen)?

Upvotes: 2

Ashvin
Ashvin

Reputation: 9027

Here is the complete code written in Swift 5 to support earlier version of iOS 14 and later

This method is deprecated from iOS 14

public init(documentTypes allowedUTIs: [String], in mode: UIDocumentPickerMode)
  1. Write this code in your button action

    @IBAction func importItemFromFiles(sender: UIBarButtonItem) {
    
             var documentPicker: UIDocumentPickerViewController!
             if #available(iOS 14, *) {
                 // iOS 14 & later
                 let supportedTypes: [UTType] = [UTType.image]
                 documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes)
             } else {
                 // iOS 13 or older code
                 let supportedTypes: [String] = [kUTTypeImage as String]
                 documentPicker = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import)
             }
             documentPicker.delegate = self
             documentPicker.allowsMultipleSelection = true
             documentPicker.modalPresentationStyle = .formSheet
             self.present(documentPicker, animated: true)
         }
    
  2. Implement Delegates

// MARK: - UIDocumentPickerDelegate Methods

extension MyViewController: UIDocumentPickerDelegate {
   func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {

    for url in urls {
        
        // Start accessing a security-scoped resource.
        guard url.startAccessingSecurityScopedResource() else {
            // Handle the failure here.
            return
        }
        
        do {
            let data = try Data.init(contentsOf: url)
            // You will have data of the selected file
        }
        catch {
            print(error.localizedDescription)
        }
        
        // Make sure you release the security-scoped resource when you finish.
        defer { url.stopAccessingSecurityScopedResource() }
    }        
  }

   func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
      controller.dismiss(animated: true, completion: nil)
  }
}

Upvotes: 1

Sudhi 9135
Sudhi 9135

Reputation: 765

I was facing similar issue. File in the url path got removed after sometime. I solved by using .open UIDocumentPickerMode instead of .import.

let importMenu = UIDocumentPickerViewController(documentTypes: [String(kUTTypeData)], in: .open)

importMenu.delegate = self

importMenu.modalPresentationStyle = .formSheet

present(importMenu, animated: true, completion: nil)

In this case url path of the selected document will change in the below delegate method.

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {

    print("url documentPicker:",urls)

}

You can observe now the path has been changed. Now we are getting exact path where the file resided. So it will not be removed after sometime. For iPad's and in simulators files resides under "File Provider Storage" folder and for iPhone files will come under "Document" folder. With this path you can get extensions and name of the files as well.

Upvotes: 2

Najdan Tomić
Najdan Tomić

Reputation: 2111

Description

Similar problem occurred to me. I have document picker initialized like this:

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

Which means that files are copied to app_id-Inbox directory after they are selected in documentPicker. When delegate method documentPicker(_:didPickDocumentsAt:) gets called it gives URLs which are pointing to files that are located in app_id-Inbox directory.

Problem

After some time (without closing app) those URLs were pointing to files that are not existing. That happened because app_id-Inbox in tmp/ folder was cleared meanwhile. For example I pick documents, show them in table view and leave iPhone on that screen for like a minute, then when I try to click on specific documents which opens file in QLPreviewController using URL provided from documentPicker it returns file not existing.

This seems like a bug because Apple's documentation states following here

UIDocumentPickerModeImport

The URLs refer to a copy of the selected documents. These documents are temporary files. They remain available only until your application terminates. To keep a permanent copy, move these files to a permanent location inside your sandbox.

It clearly says until application terminates, but in my case that was around minute of not opening that URL.

Workaround

Move files from app_id-Inbox folder to tmp/ or any other directory then use URLs that are pointing to new location.

Swift 4

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
    let newUrls = urls.compactMap { (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
}

Although system will take care of /tmp directory it's recommended to clear its content when it's not needed anymore.

Upvotes: 45

Utkarsh Goel
Utkarsh Goel

Reputation: 245

while using document picker, for using file system do convert url to file path as below:

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

Upvotes: 5

R3dHatCat
R3dHatCat

Reputation: 101

I don't think the problem is that tmp directory is clean up with some way.

If you work with simulator and open a file from icloud, you will see that the file exists in app-id-Inbox and NOT clean/deleted. So when you try to read the file or copy and take an error that file not exist, i think that is a security problem , because you can see it that the file is still there.

On import mode of DocumentPickerViewController i resolve it with this (sorry i will paste c# code and not swift because i have it in front of me)

inside DidPickDocument method that returns the NSUrl i did

NSData fileData = NSData.FromUrl(url);

and now you have "fileData" variable that has the file data.

then you can copy them in a custom folder of yours in isolated space of app and it works just fine.

Upvotes: 8

Jesper
Jesper

Reputation: 47

I had the same problem, but it turned out I was deleting the Temp directory on view did appear. So it would save then delete when appearing and properly call documentPicker:didPickDocumentAtURL: only url would be pointing at the file I had deleted.

Upvotes: 1

Related Questions