Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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"}}
like image 318
imgx64 Avatar asked May 09 '16 06:05

imgx64


Video Answer


6 Answers

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.

like image 140
Najdan Tomić Avatar answered Oct 02 '22 00:10

Najdan Tomić


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.

like image 29
R3dHatCat Avatar answered Oct 02 '22 00:10

R3dHatCat


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
like image 22
Utkarsh Goel Avatar answered Oct 01 '22 23:10

Utkarsh Goel


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.

like image 29
Jesper Avatar answered Oct 01 '22 22:10

Jesper


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.

like image 22
Sudhi 9135 Avatar answered Oct 01 '22 23:10

Sudhi 9135


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)
  }
}
like image 27
Ashvin A Avatar answered Oct 02 '22 00:10

Ashvin A