iOS应用使用FileManager写入外部设备无权限问题求助:替代UIDocumentPicker实现文件传输进度显示
解决iOS外部设备文件写入权限问题并实现进度显示
我来帮你搞定这个权限问题,同时实现大文件传输的进度跟踪需求。
首先,你遇到权限错误的核心原因是:用forOpeningContentTypes选中的外部文件夹URL属于安全作用域资源,iOS沙盒默认只给你读取权限,没有写入权限;而之前用forExporting的方式是系统帮你处理了整个写入流程的权限,所以不会报错。
下面是具体的解决方案,分两步走:获取写入权限 + 实现带进度的文件复制/移动。
1. 获取外部文件夹的写入权限
在处理选中的文件夹URL时,必须先调用startAccessingSecurityScopedResource()来获取安全作用域的写入权限,用完后记得调用stopAccessingSecurityScopedResource()释放,避免资源泄漏。
2. 实现带进度的文件传输
FileManager的moveItem是同步操作,而且无法跟踪进度,所以我们改用文件流(InputStream/OutputStream) 逐块复制文件,这样就能实时计算传输进度,同时避免阻塞主线程。
完整代码示例
首先修改documentPicker代理方法:
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let targetFolderURL = urls.first else { print("未选中任何文件夹") return } // 申请安全作用域的访问权限(必须步骤) guard targetFolderURL.startAccessingSecurityScopedResource() else { print("无法获取文件夹写入权限,请检查设备权限设置") return } // 确保方法结束后释放权限 defer { targetFolderURL.stopAccessingSecurityScopedResource() } let sourceFileURL = usbSendungURL[0] // 拼接目标文件路径(保留原文件名) let targetFileURL = targetFolderURL.appendingPathComponent(sourceFileURL.lastPathComponent) // 后台线程执行文件复制,避免阻塞UI DispatchQueue.global(qos: .background).async { do { try self.copyFileWithProgress(from: sourceFileURL, to: targetFileURL) { progress in // 切回主线程更新进度条 DispatchQueue.main.async { self.progressView.progress = progress } } // 如果需要移动(复制后删除原文件),添加下面这行 // try FileManager.default.removeItem(at: sourceFileURL) print("文件传输完成") } catch { print("传输失败:\(error.localizedDescription)") } } }
然后实现带进度的复制方法:
func copyFileWithProgress(from sourceURL: URL, to targetURL: URL, progressHandler: @escaping (Float) -> Void) throws { // 创建输入输出流 guard let inputStream = InputStream(url: sourceURL), let outputStream = OutputStream(url: targetURL, append: false) else { throw NSError(domain: "FileTransferError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法创建文件流"]) } inputStream.open() outputStream.open() // 确保方法结束后关闭流 defer { inputStream.close() outputStream.close() } let bufferSize = 1024 * 1024 // 1MB缓冲区,可根据设备性能调整 let fileSize = try sourceURL.resourceValues(forKeys: [.fileSizeKey]).fileSize ?? 0 var totalBytesCopied: Int64 = 0 // 分配缓冲区内存 let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize) defer { buffer.deallocate() } // 逐块读取并写入 while inputStream.hasBytesAvailable { let bytesRead = inputStream.read(buffer, maxLength: bufferSize) if bytesRead < 0 { throw inputStream.streamError ?? NSError(domain: "FileTransferError", code: -2, userInfo: [NSLocalizedDescriptionKey: "读取源文件失败"]) } else if bytesRead == 0 { break // 读取完成 } // 确保所有读取的字节都写入目标文件 var bytesWritten = 0 while bytesWritten < bytesRead { let writeResult = outputStream.write(buffer + bytesWritten, maxLength: bytesRead - bytesWritten) if writeResult < 0 { throw outputStream.streamError ?? NSError(domain: "FileTransferError", code: -3, userInfo: [NSLocalizedDescriptionKey: "写入目标文件失败"]) } bytesWritten += writeResult } // 更新进度 totalBytesCopied += Int64(bytesWritten) if fileSize > 0 { let progress = Float(totalBytesCopied) / Float(fileSize) progressHandler(progress) } } // 最后强制进度到100% progressHandler(1.0) }
关键注意点
- 安全作用域权限:
startAccessingSecurityScopedResource()必须调用,且不能忘记释放,否则会导致权限泄漏。 - 异步执行:文件传输是耗时操作,一定要放在后台队列,更新UI必须切回主线程。
- 缓冲区大小:1MB是比较均衡的设置,太大可能占用过多内存,太小会增加IO次数,可根据你的文件大小调整。
- 移动vs复制:如果是要移动文件,复制完成后调用
removeItem(at:)删除源文件即可,但要确保源文件有删除权限。
这样既解决了外部设备的写入权限问题,又能实时更新传输进度,完美适配大文件的需求。
内容的提问来源于stack exchange,提问作者user12719663
相关产品推荐
相关产品推荐

