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"}}
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.
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.
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.
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.
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
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.
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.
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)
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)
}
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)
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With