如何在iOS 14及以下版本的应用程序中添加文件选择器

6

我是iOS开发的新手,因此我在这里展示和询问的一些事情可能很愚蠢,请不要生气 :) 所以,我需要在我的应用程序中添加从本地存储选择文件的支持。该功能将用于选择文件->编码为Base64,然后发送到远程服务器。目前,我在向我的应用程序添加此功能时遇到了一些问题。我找到了 this tutorial 并按照其中提到的所有步骤进行了操作:

  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)
    }
    
通常文件选择器会出现在模拟器屏幕上,但我在XCode中看到了警告:
'init(documentTypes:in:)' was deprecated in iOS 14.0

我访问了官方指南,在那里也找到了关于废弃某些方法的类似信息。那么,我该如何解决文件选择的问题,以便与最新的iOS版本完全兼容?另一个问题是 - 我如何对所选文件进行编码?现在我有选择文件并打印其位置的功能,但我需要获取其数据(例如名称、内容)以进行编码等操作。也许有人遇到过类似的问题并知道解决方案?我需要将其添加到普通的视图控制器中,所以当我尝试添加此实现时:
UIDocumentPickerViewController

我看到了这样的错误消息:

Multiple inheritance from classes 'UIViewController' and 'UIDocumentPickerViewController'

我会很高兴得到任何信息:教程或建议 :)


在那里找到了类似的线程 -> https://dev59.com/OlIG5IYBdhLWcg3w2VXM . 希望能有所帮助。 - Arsenic
这个回答解决了你的问题吗?iOS 14中UIDocumentPickerViewController的初始化 - WhiteSpidy.
@AppDev,稍等一下,我正在检查 :) - Andrew
@AppDev,总的来说它还不错,但我仍然不明白如何从选取的文件中获取文件数据?你知道相关信息吗? - Andrew
2个回答

3

我决定发布自己的问题解决方案。由于我是iOS开发新手,我的答案可能存在一些逻辑问题:) 首先,我添加了一些对话框,在按下附加按钮后选择文件类型:

@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)
    }

当用户选择“文件”时,他将被移动到普通目录,我在那里处理他的选择。
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")
        }
    }

从上面的范围可以看出,我创建了一个特殊的字典来存储数据以便将其发送到服务器。在这里,您还可以看到编码为Base64。

当用户在警告对话框中按下照片/视频项时,他将被移动到图库以选择图片:

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)
    }

通过我的方法,所有文件内容将被编码为base64字符串。

P.S. 我也很高兴能向@MaticOblak致敬,因为他给了我研究和最终解决问题的起点。他的解决方案也不错,但我已经成功地以更加方便的方式解决了我的问题 :)


1
一旦您拥有文件URL,您可以使用该URL检索其包含的数据。当您拥有数据时,您可以将其转换为Base64并发送到服务器。您没有提供有关如何将其发送到服务器的信息,但其余部分可能类似于以下内容:
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#> } }
}

简单来说:

要获取文件数据,您可以使用Data(contentsOf: url)。这种方法甚至适用于远程文件,因此您可以在任何您可以访问的互联网上的图像链接的URL上使用它。重要的是要知道,此方法将暂停您的线程,这通常不是您想要的。

为了避免中断当前线程,我们使用DispatchQueue(label: "DownloadingFileData." + UUID().uuidString)创建一个新队列。队列的名称并不是非常重要,但在调试时可能会有用。

当接收到数据后,我们使用data.base64EncodedString()将其转换为Base64字符串,然后可以将该数据发送到服务器。您只需要填写TODO:部分即可。

检索文件数据可能会出现一些错误。可能是访问限制、文件不存在或没有互联网连接... 这由抛出异常处理。如果try语句由于任何原因失败,则执行catch部分,您会收到一个错误。

由于所有这些都在后台线程上完成,通常意义上回到主线程是有意义的。这就是finish函数所做的事情。如果您不需要它,可以简单地将其删除,并且代码如下:

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)
        }
    }
}

在这种方法中还有其他需要考虑的事情。例如,如果用户选择多个文件,则每个文件都将打开自己的队列并开始处理过程。这意味着,如果用户选择多个文件,则可能在某些时候会加载许多或全部文件到内存中。这可能会占用太多内存并导致应用程序崩溃。你需要决定是否采用这种方法,或者你希望序列化处理过程。使用队列进行序列化非常简单,你只需要一个单独的队列即可:

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)
        }
    }
}

现在一个操作完成后才会开始另一个操作。但这可能仅适用于获取文件数据并将其转换为base64字符串。如果上传在另一个线程上完成(通常如此),则仍可能有多个正在进行的请求,其中可能包含上传所需的所有数据。

谢谢您的回复,我正在通过Alamofire将数据发送到服务器,但我认为这在从文件检索数据的一般机制中并不是很重要,但是您能否添加有关获取文件名和大小的信息,因为我在您的答案中没有看到它?谢谢 :) - Andrew
1
@Andrew 不确定你是否有这个要求。但是你可以通过 FileManager.default.attributesOfItem(atPath: url.path) 得到大部分信息。至于文件名和扩展名,甚至 URL 也有相应的方法。 - Matic Oblak
1
一般来说,我已经添加了您的代码,但是我有两个问题:1.我必须单独添加所有可能的文件类型,还是可以添加一个通用文件类型?2.在选择图像后,我遇到了这样的错误 - [DocumentManager] Failed to associate thumbnails for picked URL,这种错误可能是什么原因引起的? - Andrew
@Andrew,我会为两者提出独立的问题。其他人可能比我更能帮你解决这个问题。过去我没有处理过太多的文档API。 - Matic Oblak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接