Swift Concurrency 实现音频文件顺序处理时UI更新异常及同步执行问题求助
兄弟,太懂你从PowerShell转Swift并发的那种懵圈感了!刚上手SwiftUI异步任务的时候,踩这种基础坑简直太正常了,我来帮你捋捋问题出在哪,再给你改改代码。
首先说你遇到的核心问题:UI只显示最后一个文件名、所有“转录任务”并行跑,根源出在你的processFiles方法里——你循环每个文件都开了一个独立的Task,这些任务会同时启动,相当于所有processFile几乎在同一时间设置fileName,最后一个文件的名字自然会覆盖前面所有的;同时所有模拟耗时的定时器也一起触发,控制台就会一堆输出挤在一起。
再说说你那个Task.synchronous扩展,这玩意儿完全违背了Swift并发的设计思路啊!它用信号量阻塞当前线程,配合DispatchQueue.main.asyncAfter很容易搞出死锁——比如你后来试的Task.sleep在这个同步方法里挂了,就是因为主线程被信号量堵死了,Task.sleep的异步逻辑根本没法执行,信号量永远收不到signal,直接卡死。
接下来给你改出正确的顺序执行版本:
第一步:让任务真正顺序执行
把processFiles改成异步函数,在同一个Task里顺序遍历文件,等待每个文件处理完成后再取下一个:
private func processFiles() async { for file in files { await processFile(file) } }
然后在onAppear里用一个Task来调用它,别直接同步执行:
.onAppear { Task { files = await getFiles() // 顺便把文件读取也移到后台,避免卡UI await processFiles() } }
第二步:正确模拟耗时任务
删掉那个坑爹的Task.synchronous扩展,用Swift并发原生的Task.sleep来模拟耗时,这才是符合Async/Await规范的写法:
private func processFile(_ url: URL) async { // 更新UI,SwiftUI会自动把@State的更新切回主线程 fileName = url.lastPathComponent let seconds = Double.random(in: 10.0...20.0) let nanoseconds = UInt64(seconds * 1_000_000_000) do { // 模拟耗时任务,这里替换成真实的SFSpeech识别逻辑就行 try await Task.sleep(nanoseconds: nanoseconds) print("\(url.lastPathComponent) 耗时 \(seconds) 秒完成") } catch { // 处理任务取消或者错误(比如用户中途退出) print("\(url.lastPathComponent) 处理失败:\(error.localizedDescription)") } }
第三步:优化文件读取(可选但推荐)
原来的getFiles是同步方法,如果你的音频文件夹里文件很多,在onAppear里同步调用会短暂阻塞UI,把它改成异步方法移到后台执行:
private func getFiles() async -> [URL] { do { let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! let path = documentsURL.appendingPathComponent("Voice Memos").absoluteURL let contents = try FileManager.default.contentsOfDirectory(at: path, includingPropertiesForKeys: nil, options: []) return contents .filter { $0.pathExtension == "m4a" } .sorted { $0.path > $1.path } } catch { print("读取文件失败:\(error.localizedDescription)") return [] } }
额外说下并行的问题
你提到SFSpeechURLRecognitionRequest不支持并行,那现在的顺序执行就是最稳妥的方案。如果以后官方开放了并行能力,或者你想自己控制并发数(比如同时处理2个文件),可以用TaskGroup的withLimitedConcurrency来实现,不过那是后话了,先把基础的顺序跑通再说~
备注:内容来源于stack exchange,提问作者Gordon

