LochNessMonster
LochNessMonster

Reputation: 441

SWIFTUI - File Not Found error when trying to import a file from a cloud file provider like OneDrive and GoogleDrive

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:

SwiftUI - File Not Found when trying to import from Google Drive

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

Answers (1)

LochNessMonster
LochNessMonster

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

Related Questions