如何在 iOS 14+ 及更低版本上将文件选择器添加到应用程序

Posted

技术标签:

【中文标题】如何在 iOS 14+ 及更低版本上将文件选择器添加到应用程序【英文标题】:How to add file picker to the app on iOS 14+ and lower 【发布时间】:2020-10-01 05:02:01 【问题描述】:

我是 ios 开发的新手,所以我将在这里展示和询问的一些事情可能很愚蠢,请不要生气 :) 所以,我需要在我的应用程序中添加从本地存储中选择文件的支持。此功能将用于选择文件 -> 编码为 Base64,然后发送到远程服务器。现在我在将这个功能添加到我的应用程序时遇到了一些问题。我找到了this tutorial 并做了这里提到的一切:

    添加导入 - import MobileCoreServices

    添加实现 - UIDocumentPickerDelegate

    添加此代码范围以显示选择器:

    let documentPicker = UIDocumentPickerViewController(documentTypes: [String(kUTTypeText),String(kUTTypeContent),String(kUTTypeItem),String(kUTTypeData)], in: .import)
    documentPicker.delegate = self
    self.present(documentPicker, animated: true)
    

    还添加了所选文件的处理程序:

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

一般文件选择器出现在模拟器屏幕上,但我在 XCode 中看到警告:

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

我访问了official guideline,在这里还找到了有关弃用某些方法的类似信息。那么,如何通过与最新iOS版本完全兼容的方式来解决我的文件选择问题。还有另一个问题 - 我如何编码选定的文件?现在我有能力选择文件并打印它的位置,但我需要获取它的数据,如名称、编码内容和其他一些数据。也许有人面临类似的问题并知道解决方案?我需要在普通的viewcontroller中添加它,所以当我尝试添加这个实现时:

UIDocumentPickerViewController

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

Multiple inheritance from classes 'UIViewController' and 'UIDocumentPickerViewController'

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

【问题讨论】:

在那里找到了类似的线程 -> ***.com/questions/62653008/… 。希望对你有帮助 这能回答你的问题吗? Initialization of UIDocumentPickerViewController in iOS 14 @AppDev.,等一下,我正在检查 :) @AppDev.,一般来说效果很好,但我还是不明白如何从挑选的文件中获取文件数据?你知道吗? 【参考方案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)
        
    

你会使用它

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

分解:

要获取文件数据,您可以使用Data(contentsOf: url)。此方法甚至适用于远程文件,因此您可以例如在您可以访问的 Internet 上的任何地方使用图像链接的 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 不确定您是否要求这样做。但是您可以通过FileManager.default.attributesOfItem(atPath: url.path) 获得大部分内容。至于文件名和扩展名,即使URL 也有相应的方法。 一般我已经添加了您的代码,但我有两个问题: 1. 我必须单独添加所有可能的文件类型,或者可以添加一种通用文件类型? 2. 选择图片后出现这样的错误-[DocumentManager] Failed to associate thumbnails for picked URL,请问是什么原因导致的? @Andrew 我会为这两个问题分别提出问题。因为其他人可能比我更好地为你解决这个问题。过去我没有过多处理文档 API。【参考方案2】:

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

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

然后当用户选择File 时,他将被移动到我处理他的选择的普通目录:

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 的编码。

当用户在警报对话框中按下Photo/Video 项目时,他将被移动到图库以进行图片选择:

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字符串。

附:我也很高兴@MaticOblak,因为他向我展示了我的研究和最终解决方案的起点。他的解决方案也不错,但我设法以对我的项目更方便的方式解决了我的问题:)

【讨论】:

以上是关于如何在 iOS 14+ 及更低版本上将文件选择器添加到应用程序的主要内容,如果未能解决你的问题,请参考以下文章

iOS5 情节提要错误:情节提要在 iOS 4.3 及更低版本上不可用

iOS BLE 蓝牙 8.1 及更低版本 BLE 订阅特征通知无响应

AWS Device Farm目前支持iOS 10.3.3及更低版本的XCTest

从 iOS 11 上的左侧菜单导航后 Xcode 9 导航栏问题不在 iOS 10.3 及更低版本上

在 IOS 9 及更低版本中创建 nsmanagedcontext

“使用 Apple 登录”是不是允许应用向后兼容 iOS 12 及更低版本?