I am developing a feature for my app where user can upload a PDF from shared storage in device (Drive, onDrive, Dropbox,..) I am using Kotlin Multiplatform 2.0.20
This is the code i am using to upload the PDF for iOS, it works fine for files selected from iCloud, Downloaded locally in the device and Dropbox but NOT for Google Drive
and OneDrive
val documentPickerController = UIDocumentPickerViewController(
forOpeningContentTypes = listOf(UTTypePDF)
)
documentPickerController.allowsMultipleSelection = false
val documentDelegate = remember {
object : NSObject(), UIDocumentPickerDelegateProtocol {
override fun documentPicker(
controller: UIDocumentPickerViewController,
didPickDocumentAtURL: NSURL
) {
val accessing =
didPickDocumentAtURL.startAccessingSecurityScopedResource()
val data = try {
NSData.dataWithContentsOfURL(didPickDocumentAtURL)
} catch (e: Error) {
e.printStackTrace()
null
}
if (accessing == true) {
didPickDocumentAtURL.stopAccessingSecurityScopedResource()
}
controller.dismissViewControllerAnimated(true, null)
}
}
}
When debugging i find that the problem in this line : NSData.dataWithContentsOfURL(didPickDocumentAtURL)
it returns null
no exception is thrown and even putting it in try/catch
block did't help to understand the error behind the null value.
I don't have an expert knowledge of ObjectiveC or Swift so i am reaching out for your help :)
I found this is exactly a similar issue: https://www.hackingwithswift.com/forums/swiftui/error-domain-nsposixerrordomain-code-2-no-such-file-or-directory-when-using-fileimporter-then-read-the-content/20397
except they use SwiftUI, but i am not so i wonder how can i fix it in my case.
I appreciate if someone faced similar issue and can share his/her thoughts :)
Thank you!
I managed to find the reason behind this issue and a solution.
The problem: in iOS cloud storage (Google Drive, OneDrive, Dropbox,..) provide placeholders not the real URI to the file and unless the file is downloaded locally you cannot get the Data of the file. Even it seems to you that you can access files from Google Drive, or any other cloud storage from the shared storage in iOS u can not get the data only if they are downloaded in the device.
The solution: There are 2 solution for this problem:
Use each Cloud service/API separately to manage access remote files. that way you need to implement each API separately.
Display a preview of the file using documentInteractionController
and it will take care of loading the file, and then you got access to data.
I personally found the second solution more efficient, because in my case i just need the data of the selected file and displaying the file to the user is not problem for me so i am not limited to implement the logic only in the background.
here is my solution:
lateinit var documentInteractionDelegate: DocumentInteractionControllerDelegate
val documentInteractionController = UIDocumentInteractionController()
val documentPickerController = UIDocumentPickerViewController(
forOpeningContentTypes = listOf(UTTypePDF)
)
documentPickerController.allowsMultipleSelection = false
val documentPickerDelegate = remember {
object : NSObject(), UIDocumentPickerDelegateProtocol {
override fun documentPicker(
controller: UIDocumentPickerViewController,
didPickDocumentAtURL: NSURL
) {
val accessing = didPickDocumentAtURL.startAccessingSecurityScopedResource()
if (accessing) {
documentInteractionController.URL = didPickDocumentAtURL
documentInteractionDelegate =
DocumentInteractionControllerDelegate(
fileUrl = documentInteractionController.URL,
handelDocInfoCallback = handelDocInfo
)
documentInteractionController.delegate = documentInteractionDelegate
presentDocumentInteractionController(documentInteractionController)
didPickDocumentAtURL.stopAccessingSecurityScopedResource()
}
controller.dismissViewControllerAnimated(true, null)
}
override fun documentPickerWasCancelled(controller: UIDocumentPickerViewController) {
println("$TAG Document picker was cancelled.")
}
}
}
and this the DocumentInteractionControllerDelegate
class:
@OptIn(ExperimentalForeignApi::class)
class DocumentInteractionControllerDelegate(
private val fileUrl: NSURL?,
val handelDocInfoCallback: (DocumentInfo) -> Unit
) : NSObject(),
UIDocumentInteractionControllerDelegateProtocol {
/**
* Specifies the UIViewController that will host the preview.
*/
override fun documentInteractionControllerViewControllerForPreview(controller: UIDocumentInteractionController): UIViewController {
println("Providing the view controller for document preview.")
return UIApplication.sharedApplication.keyWindow?.rootViewController ?: UIViewController()
}
/**
* Optional: Called when the preview is about to begin
* Called before the document preview begins.
*/
override fun documentInteractionControllerWillBeginPreview(controller: UIDocumentInteractionController) {
println("Document preview will begin.")
}
/**
* Called when the document preview is done (e.g., dismissed)
*/
@OptIn(BetaInteropApi::class)
override fun documentInteractionControllerDidEndPreview(controller: UIDocumentInteractionController) {
println("Document preview ended. ${fileUrl?.path}")
// You can perform any action here after the preview has ended, like notifying the user or handling another task
val accessible = fileUrl?.startAccessingSecurityScopedResource()
println("Document preview ended. - accessible $accessible")
if (accessible == true) {
memScoped {
val errorPtr: ObjCObjectVar<NSError?> = alloc<ObjCObjectVar<NSError?>>()
val newData = fileUrl?.path?.let {
NSData.dataWithContentsOfFile(
it,
NSDataReadingUncached,
errorPtr.ptr
)
}
println("$TAG errorPtr: ${errorPtr.value}")
println("$TAG newData: ${newData?.toByteArray()?.size}")
}
fileUrl?.stopAccessingSecurityScopedResource()
}
}
/**
* Returns the UIView that acts as the anchor for the preview animation.
*/
override fun documentInteractionControllerViewForPreview(controller: UIDocumentInteractionController): UIView? {
println("Providing view for document preview.")
return UIApplication.sharedApplication.keyWindow?.rootViewController?.view
}
}
fun presentDocumentInteractionController(documentInteractionController: UIDocumentInteractionController): Boolean {
// Present the document interaction controller
val displayed = try {
documentInteractionController.presentPreviewAnimated(true)
} catch (e: Exception) {
e.printStackTrace()
false
}
return displayed
}