SwiftUI后台任务中FileManager/URLSession无法写入documentDirectory问题
问题根因
这问题和FileManager写入权限半毛钱关系都没有,三个逻辑错误叠在一起才会导致前后台表现不一致:
- 第一,
loadCSV的时序完全写反了。URLSessionDownloadTask是异步执行的,你调用task.resume()之后代码会立刻往下走,根本不会等下载完成、文件写入结束,就直接去读本地的testcsv.csv解析返回——等于你每次读到的都是上一次存的旧文件。前台时网络状态好,偶尔能撞上下载刚好写完的时机,看起来功能正常;后台任务本身系统给的可用执行时间就极短,根本等不到下载回调触发。 - 第二,你用的默认配置
URLSession根本不支持后台下载。只要App被系统挂起,默认Session里的所有网络请求会被直接掐断,拿不到下载完成的临时文件,自然走不到FileManager写入的逻辑。 - 第三,你写的所有版本的
handleAppRefresh都没正确管理BGProcessingTask生命周期:既没实现任务过期的回调处理,也没在下载、写入、数据处理全流程跑完之后主动调用task.setTaskCompleted(success:),系统会在分配的后台时间耗尽后直接终止进程,写入操作根本来不及执行。
修复方案
按下面步骤改就能解决:
- 先把
loadCSV改成真正的异步方法,用await等下载、文件写入全完成之后,再读文件解析返回,不要在下载还没跑完的时候就读本地文件。核心改法参考:
func loadCSV(from csvName: String) async throws -> [CSVEvent] { // 本地文件存储路径 let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let destinationFileUrl = documentsUrl.appendingPathComponent("testcsv.csv") // 远程CSV地址 let fileURL = URL(string: "https://example.com/testcsv.csv")! // 用后台Session配置,需要给配置设置全局唯一标识符 let sessionConfig = URLSessionConfiguration.background(withIdentifier: "com.yourapp.csvdownload") sessionConfig.isDiscretionary = false sessionConfig.sessionSendsLaunchEvents = true let session = URLSession(configuration: sessionConfig) // 等待下载完成 let (tempLocalUrl, response) = try await session.download(from: fileURL) guard let httpRes = response as? HTTPURLResponse, httpRes.statusCode == 200 else { throw URLError(.badServerResponse) } // 下载完成后再写入文件 if FileManager.default.fileExists(atPath: destinationFileUrl.path) { _ = try FileManager.default.replaceItemAt(destinationFileUrl, withItemAt: tempLocalUrl) } else { try FileManager.default.moveItem(at: tempLocalUrl, to: destinationFileUrl) } // 等文件写完再读内容解析 let data = try String(contentsOf: destinationFileUrl, encoding: .utf8) var csvToStruct = [CSVEvent]() var rows = data.components(separatedBy: "\n") guard !rows.isEmpty else { return [] } rows.removeFirst() for row in rows where !row.isEmpty { let csvColumns = row.components(separatedBy: ",") let csveventStruct = CSVEvent(raw: csvColumns) csvToStruct.append(csveventStruct) } return csvToStruct }
- 修正BGProcessingTask的生命周期管理,持有当前task的引用,在任务过期、逻辑执行完成的时候正确标记任务结束:
// 持有当前后台任务的引用 var currentBgTask: BGProcessingTask? func handleAppRefresh(task: BGProcessingTask) { scheduleAppRefresh() currentBgTask = task // 处理任务超时过期场景 task.expirationHandler = { // 取消所有未完成的下载、解析逻辑 task.setTaskCompleted(success: false) self.currentBgTask = nil } Task.detached { do { let events = try await self.loadCSV(from: "Eventtest").filter { !self.dateInPast(value: $0.date) } self.pullData(events: events) // 所有逻辑跑完再标记任务完成 self.currentBgTask?.setTaskCompleted(success: true) self.currentBgTask = nil } catch { print(error) self.currentBgTask?.setTaskCompleted(success: false) self.currentBgTask = nil } } }
- 在Info.plist里补全后台下载需要的权限:开启
Background fetch和Background processing项,保证注册BGProcessingTask的identifier和代码里的配置完全一致。
额外注意点
- 不要在后台任务里用
DispatchQueue.global().async包裹核心逻辑,GCD队列无法被系统跟踪任务完成状态,很容易执行到一半就被系统挂起。 - 后台下载不要用默认的URLSession配置,必须用background类型的配置,否则App一旦被挂起请求就会中断。
- 所有异步逻辑必须等执行完成之后再调用
setTaskCompleted,不要提前返回。
内容的提问来源于stack exchange,提问作者Al McIver
相关产品推荐
相关产品推荐

