Reputation: 441
I have the following SwiftUI code where a simple button brings up the iOS file manager and allows the user to select a CSV file to be imported. I've found that it works well for files that are stored locally on my device but if I try to select a file from Google Drive or OneDrive it gets a URL but when I then try to retrieve the data from it, it returns an error saying that the file was not found.
After a lot of head scratching, I've found that when using the file browser if I long press to bring up the context menu and then view the info for the file (which I'm guessing may be pulling it down to the phones local cache), it will then work as expected. This is shown in the following animated gif:
I've found that once I've done that caching trick, I can access the file without issue in other apps using the same code and I've also found that I can uninstall my app and reinstall it and it continues to work.
Can anyone advise on an approach using SwiftUI where I can avoid this File Not Found error when trying to import the file from Google Drive or OneDrive?
The entire code that I've been using for testing is as follows:
import SwiftUI
struct ContentView: View {
@State private var isImporting: Bool = false
@State private var fileContentString = ""
@State var alertMsg = ""
@State var showAlert = false
func reportError(error: String) {
alertMsg = error
showAlert.toggle()
}
var body: some View {
VStack {
Button(action: { isImporting = true}, label: {
Text("Select CSV File")
})
.padding()
Text(fileContentString) //This will display the imported CSV as text in the view.
}
.padding()
.fileImporter(
isPresented: $isImporting,
allowedContentTypes: [.commaSeparatedText],
allowsMultipleSelection: false
) { result in
do {
guard let selectedFileURL: URL = try result.get().first else {
alertMsg = "ERROR: Result.get() failed"
self.reportError(error: alertMsg)
return
}
print("selectedFileURL is \(selectedFileURL)")
if selectedFileURL.startAccessingSecurityScopedResource() {
//print("startAccessingSecurityScopedResource passed")
do {
print("Getting Data from URL...")
let inputData = try Data(contentsOf: selectedFileURL)
print("Converting data to string...")
let inputString = String(decoding: inputData, as: UTF8.self)
print(inputString)
fileContentString = inputString
}
catch {
alertMsg = "ERROR: \(error.localizedDescription)"
self.reportError(error: alertMsg)
print(alertMsg)
}
//defer { selectedFileURL.stopAccessingSecurityScopedResource() }
} else {
// Handle denied access
alertMsg = "ERROR: Unable to read file contents - Access Denied"
self.reportError(error: alertMsg)
print(alertMsg)
}
} catch {
// Handle failure.
alertMsg = "ERROR: Unable to read file contents - \(error.localizedDescription)"
self.reportError(error: alertMsg)
print(alertMsg)
}
}
.alert(isPresented: $showAlert, content: {
Alert(title: Text("Message"), message: Text(alertMsg), dismissButton: .destructive(Text("OK"), action: {
}))
})
}
}
The console log output is as follows:
selectedFileURL is file:///private/var/mobile/Containers/Shared/AppGroup/8F147702-8630-423B-9DA0-AE49667748EB/File%20Provider%20Storage/84645546/1aTSCPGxY3HzILlCIFlMRtx4eEWDZ2JAq/example4.csv
Getting Data from URL...
ERROR: The file “example4.csv” couldn’t be opened because there is no such file.
selectedFileURL is file:///private/var/mobile/Containers/Shared/AppGroup/8F147702-8630-423B-9DA0-AE49667748EB/File%20Provider%20Storage/84645546/1aTSCPGxY3HzILlCIFlMRtx4eEWDZ2JAq/example4.csv
Getting Data from URL...
Converting data to string...
First Name,Last Name
Luke,Skywalker
Darth,Vader
My testing has been done on a physical iPhone 12 Pro Max running iOS 14.2 and a physical iPad Air 2 running iPadOS 14.4.
Upvotes: 7
Views: 1387
Reputation: 441
I found an answer to my issue. The solution was to use a NSFileCoordinator() to force the file to be downloaded.
With the code below, if I access a file in cloud storage that hasn't been previously downloaded to the local device it will print "FILE NOT AVAILABLE" but it will now just download the file rather than throwing a file not found error.
Ideally I would like to be able to download just the file property metadata first to check how big the file is and then decide if I want to download the full file. The NSFileCoordinator has a metadata only option but I haven't worked out how to retrieve and interpret the results from that. This will do for now...
if selectedFileURL.startAccessingSecurityScopedResource() {
let fileManager = FileManager.default
if fileManager.fileExists(atPath: selectedFileURL.path) {
print("FILE AVAILABLE")
} else {
print("FILE NOT AVAILABLE")
}
var error: NSError?
NSFileCoordinator().coordinate(
readingItemAt: selectedFileURL, options: .forUploading, error: &error) { url in
print("coordinated URL", url)
let coordinatedURL = url
isShowingFileDetails = false
importedFileURL = selectedFileURL
do {
let resources = try selectedFileURL.resourceValues(forKeys:[.fileSizeKey])
let fileSize = resources.fileSize!
print ("File Size is \(fileSize)")
} catch {
print("Error: \(error)")
}
}
do {
print("Getting Data from URL...")
let inputData = try Data(contentsOf: selectedFileURL)
print("Do stuff with file....")
}
}
Upvotes: 10