iosswiftobjective-cfile-uploadkotlin-multiplatform

Upload PDF from shared local storage in iOS (Drive, OneDrive, Dropbox..) to server with Ktor Fails


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!


Solution

  • 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:

    1. Use each Cloud service/API separately to manage access remote files. that way you need to implement each API separately.

    2. 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
    }