26

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"}}
imgx64
  • 4,062
  • 5
  • 28
  • 44
  • I have a similar problem importing images from Google Drive using UIDocumentPickerViewController. A valid-looking URL is returned, but fileExistsAtPath returns nil (but only sporadically). I need to use Import mode (as you have), but I have noticed that the problem seems to go away if I switch to Open mode. Also, I believe that you only need to call startAccessingSecurityScopedResource when using Open or Move mode. In my testing, that call always returns false when using Import mode. Have you made any further headway on this since posting? – grfryling Jun 01 '16 at 22:57
  • @grfryling I settled on giving a vague error message to the user. I tried Open mode and I found I could use that non-existent url with ["ubiquitous" functions](https://developer.apple.com/library/ios/documentation/Cocoa/Reference/Foundation/Classes/NSFileManager_Class/index.html#//apple_ref/doc/uid/20000305-SW76) such as `startDownloadingUbiquitousItemAtURL:error:`. However, I didn't use it because Dropbox doesn't support Open mode. – imgx64 Jun 02 '16 at 10:08
  • Cool old known issue. This issue also exist with bookmarks where it makes bookmarkdata return nil and an error code 260 is thrown. Good one apple. – Warpzit Oct 26 '21 at 10:57

7 Answers7

44

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.

Najdan Tomić
  • 2,071
  • 16
  • 25
  • It was really easy to get the data.Thanks – SThakur Jul 23 '18 at 13:23
  • Using this approach, one of my issues got fixed. But one issue is still there. And that is: when I picked a document and tapped on my iPhone's lock button, and then come back to the application after 2 minutes, it says "file not exists." @Najdan – Muneeb Rehman Jun 02 '20 at 10:06
  • 1
    @MuneebRehman you can try to move it to any other directory to see if that helps with issue. But first check if picked documents are really in temp directory and that you are using url to document in temp directory and not URL from document picker. – Najdan Tomić Jun 02 '20 at 11:32
  • I read that tmp directory can be deleted by OS at anytime. Can that be a reason? – Muneeb Rehman Jun 02 '20 at 12:36
  • @MuneebRehman yes that can be a reason. That’s why I proposed to try with different directory to see if that helps. You can create your own in Documents folder. – Najdan Tomić Jun 02 '20 at 12:42
  • I have checked according to the above code that the file has been moved to the tmp directory. So you are asking me to create a folder and move it there? – Muneeb Rehman Jun 02 '20 at 12:54
  • @MuneebRehman I’m just suggesting to try different directory instead of tmp one. – Najdan Tomić Jun 02 '20 at 13:07
  • @NajdanTomić I have tried NSHomeDirectory() instead of NSTemporaryDirectory() but still no success. – Muneeb Rehman Jun 02 '20 at 13:54
  • @MuneebRehman Can you share the code you have for saving data to that directory and how you retrieve file from URL? You can mail me code to najdan.tomic@icloud.com, and we can continue conversation there as well. – Najdan Tomić Jun 02 '20 at 14:20
  • Hi @NajdanTomić This fixed my issue, I have one question, what do you suggest should I make my own folder or let the files there in tmp folder ? – Ammar Jun 17 '20 at 10:37
  • 1
    @Ammar if it's just for temporary use (upload to your server, send it over network) leave it in temp folder. – Najdan Tomić Jun 18 '20 at 10:02
  • How can I explain you, you saved my life bro. Thank you so much. Perfect and detailed answer. – Umair_UAS Aug 19 '20 at 18:00
  • You saved my life bro. Thank you so much ! – D.A.C. Nupun Jul 22 '23 at 00:01
8

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.

R3dHatCat
  • 101
  • 1
  • 2
  • Seems a security problem. Not sure what I am doing wrong yet, but, the file does not exist when checking with FileManager, however when I try to read it I get this: Upload preparation for claim 89A76A7F-C027-4DFA-B01A-7AEC004F6A21 completed with error: Error Domain=NSCocoaErrorDomain Code=257 "The file “test.pdf” couldn’t be opened because you don’t have permission to view it." – zumzum May 20 '21 at 18:30
  • calling startAccessingSecurityScopedResource() on the fileURL returned by the picker then allows FileManager.default.fileExists(atPath: fileURL.path) to be true. Make sure to call stopAccessingSecurityScopedResource() on it to balance the calls. – zumzum May 20 '21 at 18:35
5

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
Utkarsh Goel
  • 245
  • 2
  • 5
2

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.

Dharman
  • 30,962
  • 25
  • 85
  • 135
Sudhi 9135
  • 745
  • 8
  • 17
1

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.

Jesper
  • 47
  • 1
  • 5
  • Holy... I stumbled for this problem for several hours and this was exactly the case! Visual controller in completely separate part of application do the clean for tmp directory on viewDidAppear, completely killing files from document picker right after they created. Coincidence, that is hard to track down )) – IPv6 Aug 01 '17 at 19:04
  • Great I was not the only one! Glad it helped! – Jesper Aug 01 '17 at 22:00
1

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)
  }
}
Ashvin
  • 8,227
  • 3
  • 36
  • 53
1

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)?

user1251560
  • 109
  • 7