You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 04:24:05