6

I'm newbie in iOS development, so some things which I will show and ask here can be stupid and please don't be angry :) So, I need to add support of picking files from local storage in my app. This feature will be used for picking file -> encoding to Base64 and then sending to remote server. Right now I have some problems with adding this functionality to my app. I had found this tutorial and did everything what was mentioned here:

  1. added import - import MobileCoreServices

  2. added implementation - UIDocumentPickerDelegate

  3. added this code scope for showing picker:

    let documentPicker = UIDocumentPickerViewController(documentTypes: [String(kUTTypeText),String(kUTTypeContent),String(kUTTypeItem),String(kUTTypeData)], in: .import)
    documentPicker.delegate = self
    self.present(documentPicker, animated: true)
    
  4. and also added handler of selected file:

    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
    print(urls)
    }
    

In general file chooser appears on simulator screen, but I see warning in XCode:

'init(documentTypes:in:)' was deprecated in iOS 14.0

I visited the official guideline and here also found similar info about deprecation some method. So, how I can solve my problem with file choosing by the way which will be fully compatible with the latest iOS version. And another question - how I can then encode selected file? Right now I have an ability of file choosing and printing its location, but I need to get its data like name, content for encoding and some others. Maybe someone faced with similar problems and knows a solution? I need to add it in ordinary viewcontroller, so when I tried to add this implementation:

UIDocumentPickerViewController

I saw such error message:

Multiple inheritance from classes 'UIViewController' and 'UIDocumentPickerViewController'

I will be so pleased for any info: tutorials or advice :)

Andrew
  • 1,947
  • 2
  • 23
  • 61
  • found similar thread there -> https://stackoverflow.com/questions/62653008/initialization-of-uidocumentpickerviewcontroller-in-ios-14 . Hope it helps – Arsenic Oct 01 '20 at 05:24
  • Does this answer your question? [Initialization of UIDocumentPickerViewController in iOS 14](https://stackoverflow.com/questions/62653008/initialization-of-uidocumentpickerviewcontroller-in-ios-14) – WhiteSpidy. Oct 01 '20 at 05:49
  • @AppDev., just a moment, I'm checking :) – Andrew Oct 01 '20 at 05:57
  • @AppDev., in general it works well, but I still can't understand how to get file data from picked file? Do you know smth about it? – Andrew Oct 01 '20 at 06:03

2 Answers2

3

I decided to post my own solution of my problem. As I am new in ios development my answer can contain some logical problems :) Firstly I added some dialogue for choosing file type after pressing Attach button:

@IBAction func attachFile(_ sender: UIBarButtonItem) {
        let attachSheet = UIAlertController(title: nil, message: "File attaching", preferredStyle: .actionSheet)
        
        
        attachSheet.addAction(UIAlertAction(title: "File", style: .default,handler: { (action) in
            let supportedTypes: [UTType] = [UTType.png,UTType.jpeg]
            let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes)
            documentPicker.delegate = self
            documentPicker.allowsMultipleSelection = false
            documentPicker.shouldShowFileExtensions = true
            self.present(documentPicker, animated: true, completion: nil)
        }))
        
        attachSheet.addAction(UIAlertAction(title: "Photo/Video", style: .default,handler: { (action) in
            self.chooseImage()
        }))
        
        
        attachSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel))
        
        self.present(attachSheet, animated: true, completion: nil)
    }

then when a user will choose File he will be moved to ordinary directory where I handle his selection:

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
        var selectedFileData = [String:String]()
        let file = urls[0]
        do{
            let fileData = try Data.init(contentsOf: file.absoluteURL)
           
            selectedFileData["filename"] = file.lastPathComponent
            selectedFileData["data"] = fileData.base64EncodedString(options: .lineLength64Characters)
            
        }catch{
            print("contents could not be loaded")
        }
    }

as you can see in scope above I formed special dicionary for storing data before sending it to a server. Here you can also see encoding to Base64.

When the user will press Photo/Video item in alert dialogue he will be moved to gallery for picture selecting:

func chooseImage() {
        imagePicker.allowsEditing = false
        imagePicker.sourceType = .photoLibrary
        
        present(imagePicker, animated: true, completion: nil)
    }
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        var selectedImageData = [String:String]()
        
        
        guard let fileUrl = info[UIImagePickerController.InfoKey.imageURL] as? URL else { return }
        
        
        print(fileUrl.lastPathComponent)
        
        if let pickedImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
            selectedImageData["filename"] = fileUrl.lastPathComponent
            selectedImageData["data"] = pickedImage.pngData()?.base64EncodedString(options: .lineLength64Characters)

            
        }
        
        dismiss(animated: true, completion: nil)
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }

via my method all file content will be encoded to base64 string.

P.S. Also I'm so pleased to @MaticOblak because he showed me the initial point for my research and final solution. His solution also good, but I have managed to solve my problem in way which is more convenient for my project :)

Andrew
  • 1,947
  • 2
  • 23
  • 61
1

As soon as you have file URL you can use that URL to retrieve the data it contains. When you have the data you can convert it to Base64 and send it to server. You gave no information about how you will send it to server but the rest may look something like this:

func sendFileWithURL(_ url: URL, completion: @escaping ((_ error: Error?) -> Void)) {
    func finish(_ error: Error?) {
        DispatchQueue.main.async {
            completion(error)
        }
    }
    
    DispatchQueue(label: "DownloadingFileData." + UUID().uuidString).async {
        do {
            let data: Data = try Data(contentsOf: url)
            let base64String = data.base64EncodedString()
            // TODO: send string to server and call the completion
            finish(nil)
        } catch {
            finish(error)
        }
    }
}

and you would use it as

func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
    urls.forEach { sendFileWithURL($0) { <#Your code here#> } }
}

To break it down:

To get file data you can use Data(contentsOf: url). This method even works on remote files so you could for instance use an URL of an image link anywhere on internet you have access to. It is important to know that this method will pause your thread which is usually not what you want.

To avoid breaking the current thread we create a new queue using DispatchQueue(label: "DownloadingFileData." + UUID().uuidString). The name of the queue is not very important but can be useful when debugging.

When data is received we convert it to Base64 string using data.base64EncodedString() and this data can then be sent to server. You just need to fill in the TODO: part.

Retrieving your file data can have some errors. Maybe access restriction or file no longer there or no internet connection... This is handled by throwing. If the statement with try fails for any reason then the catch parts executes and you receive an error.

Since all of this is done on background thread it usually makes sense to go back to main thread. This is what the finish function does. If you do not require that you can simply remove it and have:

func sendFileWithURL(_ url: URL, completion: @escaping ((_ error: Error?) -> Void)) {
    DispatchQueue(label: "DownloadingFileData." + UUID().uuidString).async {
        do {
            let data: Data = try Data(contentsOf: url)
            let base64String = data.base64EncodedString()
            // TODO: send string to server and call the completion
            completion(nil)
        } catch {
            completion(error)
        }
    }
}

There are other things to consider in this approach. For instance you can see if user selects multiple files then each of them will open its own queue and start the process. That means that if user selects multiple files it is possible that at some point many or all of them will be loaded in memory. That may take too much memory and crash your application. It is for you to decide if this approach is fine for you or you wish to serialize the process. The serialization should be very simple with queues. All you need is to have a single one:

private lazy var fileProcessingQueue: DispatchQueue = DispatchQueue(label: "DownloadingFileData.main")

func sendFileWithURL(_ url: URL, completion: @escaping ((_ error: Error?) -> Void)) {
    func finish(_ error: Error?) {
        DispatchQueue.main.async {
            completion(error)
        }
    }
    
    fileProcessingQueue.async {
        do {
            let data: Data = try Data(contentsOf: url)
            let base64String = data.base64EncodedString()
            // TODO: send string to server and call the completion
            finish(nil)
        } catch {
            finish(error)
        }
    }
}

Now one operation will finish before another one starts. But that may only apply for getting file data and conversion to base64 string. If uploading is then done on another thread (Which usually is) then you may still have multiple ongoing requests which may contain all of the data needed to upload.

Matic Oblak
  • 16,318
  • 3
  • 24
  • 43
  • Thank you for your response , I'm sending data to server via Alamofire, but I think that it is not so important in general mechanics of retrieving data from file, but can you please add some info about getting file name and size, because I didn't see it in your answer? Thank you :) – Andrew Oct 01 '20 at 07:23
  • 1
    @Andrew not sure you requested that. But you can get most of that via `FileManager.default.attributesOfItem(atPath: url.path)`. As for file name and extension even `URL` has methods for that. – Matic Oblak Oct 01 '20 at 07:29
  • 1
    In general I have added your code, but I have two question: 1. I have to add all possible file types separately or it is possible to add one general file type? 2. After selection of image I have such error - `[DocumentManager] Failed to associate thumbnails for picked URL`, what can cause such error? – Andrew Oct 01 '20 at 08:13
  • @Andrew I would go with separate questions for the two. As others may probably resolve this for you better than me. I have not been dealing much with documents API in the past. – Matic Oblak Oct 01 '20 at 08:39