Reputation: 61
I'm developing an IOS app for the IPAD in as much native Swift as possible, (using Xcode 11.4.1, Swift 5, targeting IOS 13.X). I do not know Cocoa or Objective-C, and I don't have prior IOS experience. I'm coming from a C/C++ and an Android/Java background. That said, I'm having a lot of fun with Swift, and how quickly it lets me do development. Things which I expected to be hard have turned out to be easy, but one thing that seems to me like it should be easy has thrown up a wall I can't get over. In brief, I am trying to allow my users to use a File Picker to choose a document from their iPad, which my app will then upload to a website server.
My use case here is a web-based private collaboration platform - users write documents on their laptops, generally with Word, and then upload those documents to the web server, using a typical HTTP POST scenario. In the past my platform has been web-only; now, I'm trying to write an IOS app to provide a more native experience for IPad users.
The way I envision this working is: Users will write documents, using (for example) Microsoft Word on the iPad, and then, from within my Swift IOS app, the user will upload their Word document(s) to the server.
My problem appears to be access permissions. I use the following code for the Document Picker:
import SwiftUI
import MobileCoreServices
struct DocPicker: UIViewControllerRepresentable {
var callback: (URL) -> ()
private let onDismiss: () -> Void
init(callback: @escaping (URL) -> (), onDismiss: @escaping () -> Void) {
self.callback = callback
self.onDismiss = onDismiss
}
func makeCoordinator() -> Coordinator { Coordinator(self) }
func updateUIViewController(_ uiViewController: UIDocumentPickerViewController, context: UIViewControllerRepresentableContext<DocPicker>) {
}
func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
let controller = UIDocumentPickerViewController(documentTypes: [kUTTypeItem as String], in: UIDocumentPickerMode.import)
controller.allowsMultipleSelection = false
controller.delegate = context.coordinator
return controller
}
class Coordinator: NSObject, UIDocumentPickerDelegate {
var parent: DocPicker
init(_ pickerController: DocPicker) {
self.parent = pickerController
}
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
parent.callback(urls[0])
parent.onDismiss()
}
func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
parent.onDismiss()
}
}
}
I call my Document Picker using this code fragment hung off of a VStack:
.sheet(isPresented: $showFilePicker, onDismiss: {self.showFilePicker = false}) {
DocPicker(callback: self.readyFile(_:), onDismiss: { self.showFilePicker = false })
And, when the callback runs, I do in fact get a URL back which seems, after a long UUID-like path component, to point to the selected file.... or at least the last component of the URL matches the name of the file I picked.
file:///private/var/mobile/Containers/Data/Application/4B63589A-7037-4105-99F1-427444B27F40/tmp/org.me.myapp-Inbox/Document.docx
So - my app runs, I choose the upload function, the picker opens. I navigate into the Microsoft Word Documents directory, I find the file I saved, I select it. All good.
However, when I get to my upload function, I encounter an error. I build the multipart/form-data post using methods that everyone seems to use, but when I try to bring in the actual data for the file here:
do {
data1 = try NSData.init(contentsOf: URL.init(fileURLWithPath: fileURL, isDirectory: true)) as Data
}
catch { print("Error \(error)") }
I get a "File not found" error in Xcode's debugging window:
file:///private/var/mobile/Containers/Data/Application/4B63589A-7037-4105-99F1-427444B27F40/tmp/org.me.myapp-Inbox/Document.docx Error Error Domain=NSCocoaErrorDomain Code=260 "The file “Document.docx” couldn’t be opened because there is no such file." UserInfo={NSFilePath=/file:/private/var/mobile/Containers/Data/Application/4B63589A-7037-4105-99F1-427444B27F40/tmp/org.me.myapp-Inbox/Document.docx, NSUnderlyingError=0x283039bf0 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
I've read comments in a few other questions - which were off-point, but caught my attention anyway - that to access a file in this case one must "copy it to the application Sandbox." When the picker executes and the user chooses a file, I note this in the debug output:
Failed to associate thumbnails for picked URL file:///private/var/mobile/Containers/Data/Application/83576950-92C8-442C-83B5-8320C037112F/Documents/Document.docx with the Inbox copy file:///private/var/mobile/Containers/Data/Application/4B63589A-7037-4105-99F1-427444B27F40/tmp/org.me.myapp-Inbox/Document.docx: Error Domain=QLThumbnailErrorDomain Code=102 "(null)" UserInfo={NSUnderlyingError=0x2830224c0 {Error Domain=GSLibraryErrorDomain Code=3 "Generation not found" UserInfo={NSDescription=Generation not found}}}
It talks about an "Inbox copy file", and it is that "copy" URL that the picker sends back to me - but the file itself seems not to exist.
Now, I should note that I've already implemented an "Upload Image" function similar to my "Upload File" function, except the Upload Image function uses an ImagePicker. In that case, I get the Image back from the picker, I can access the Image, and it uploads perfectly, no problem. The problem is somewhere in the code I've posted above... or in my understanding, which may well be limited.
This page on the Apple website talks about asking a user to select a directory, but their code fragment doesn't seem to work when I try to paste it in (a recurring theme for me, it seems) so I can't get any farther with it.
Stack Overflow question 47902995 takes a different approach, talks about security keys but seems to rely on Cocoa, and this question has a code fragment but I don't see how to tie it in to my existing picker.
Summary:
In Android (sorry), I can give my app a "permission" that lets it view all the (open) files on the device's internal storage. I get that IOS works differently, but I do not get how it is different. I don't know enough (yet) about Xcode/IOS/Swift/SwiftUI to understand all the nuances, and have been working rather blindly so far.
So my question in a nutshell is: Can someone please tell me how to modify my code so that a user can open a Microsoft-Office-created document on local device storage, and read it, so that it can then be uploaded by my app, without violating any rules or throwing any errors?
TIA
Upvotes: 3
Views: 10703
Reputation: 61
So just for the archive, it turns out that my original Document Picker code was working - and was working correctly - just as I had provided it.
The problem turned out to be the way I was handling the response.
My original wrong code was:
do {
data1 = try NSData.init(
contentsOf: URL.init(fileURLWithPath: fileURL, isDirectory: true)
) as Data
}
catch {
print("Error \(error)")
}
and that was the problem. Turns out that to get the data stream from the file, all I needed to do was:
do {
data1 = try Data(contentsOf: fileURL!)
}
catch {
print("Error \(error)")
}
That was it! When I did that, I was able to read the contents of the file, and correctly upload them via POST to my server.
Thanks to all who read my question, and to Prafulla for their response!
Upvotes: 3
Reputation: 771
In iOS, You cannot access anything outside App sandbox without specific app permission.
If you want to pick a document from user Local File, iCloud, or Google drive(if a user has it installed) or any other cloud-based storage you need to Use MobileCoreServices.
Following is The answer which provides steps How to do it:
https://stackoverflow.com/a/42370660/9838937
The above answer is WRT UIKit, not SwiftUI. Which will provide you UIDocumentMenuViewController. Now you need to wrap this controller Using the following, to make it work with SwiftUI.
https://www.hackingwithswift.com/books/ios-swiftui/wrapping-a-uiviewcontroller-in-a-swiftui-view
Upvotes: 0