SwiftUI中数据解析阶段ProgressView不更新问题排查
问题
在SwiftUI中使用ProgressView展示在线下载数据的解析状态时,遇到以下异常:
- 解析函数能在控制台正确打印进度百分比,但ProgressView完全不更新
- 下载数据阶段ProgressView更新正常,但进入
parseData阶段后,视图仅显示“Parsing Data”文本,进度数值无变化 - 控制台能正常输出进度数据,说明状态值已正确传入视图逻辑
- 手动添加短暂延迟后,ProgressView可正常更新
相关代码
下载与解析逻辑
// MARK: - Download Data func downloadData(lista: PlayListModel, completion: @escaping (_ stage: DownloadStage, _ progress: Double, _ total: Int) -> Void) async throws { do { var m3uString = "" // 下载阶段,ProgressView更新正常 m3uString = try await requestM3UListAlamo(from: lista.playlistUrl, progressHandler: { progress in completion(.downloadData, progress, 100) }) completion(.parseData, 0, 0) // 从此处开始UI停止更新 let group = try await sdparsePlaylist(m3uContent: m3uString) { progress, total in completion(.parseData, progress, Int(total)) } completion(.savingData, 0.0, 0) try await saveData(lista: lista, grouped: group) { stage, result in completion(stage, 100, 100) } } catch { print("Download failed with error: \(error.localizedDescription)") completion(.fail, 0, 0) throw error } } func sdparsePlaylist(m3uContent: String, completion: @escaping (_ progress: Double, _ total: Int) -> Void) async throws -> [String: [PlaylistItem]] { print("4-------------- parsing playlist --------------") var appoggio: [String: [PlaylistItem]] = [:] let createDate = Date() do { let playlist = try await self.parser.parse(m3uContent) let media = playlist.medias let totalItems = media.count var progress = 0 for item in media { let attributes = item.attributes let groupTitle = attributes.groupTitle ?? "NOT CLASSIFIED" let playlistItem = PlaylistItem(duration: item.duration, tvgId: attributes.id, tvgName: attributes.name, tvgCountry: attributes.country, tvgLanguage: attributes.language, tvgLogo: attributes.logo, tvgChno: attributes.channelNumber, tvgShift: attributes.shift, groupTitle: groupTitle, seasonNumber: attributes.seasonNumber, episodeNumber: attributes.episodeNumber, kind: item.kind.rawValue, url: item.url, lastPlay: createDate) if var existingPlaylist = appoggio[groupTitle] { existingPlaylist.append(playlistItem) appoggio[groupTitle] = existingPlaylist } else { appoggio[groupTitle] = [playlistItem] } let currentProgress = Double(progress) / Double(totalItems) * 100 completion(currentProgress, 100) print("parser background: \(currentProgress)% (\(progress + 1) / \(totalItems))") progress += 1 } } catch { print(error.localizedDescription) throw error } return appoggio }
SwiftUI视图代码
import SwiftUI import SwiftData import Alamofire struct AddNewPlaylist: View { @Environment(\.modelContext) private var modelContext @State private var playlistName = "TV" @State private var playlistUrl = "https://country.m3u" @State var downloadStage: DownloadStage = .none @State var progress: CGFloat = 0.5 @State var total = 0 @Binding var isPresented: Bool @State var bounce = false @State private var showAlert = false @State private var alertMessage = "" let backgroundParser: BackgroundParser var body: some View { VStack { Text("Add New Playlist") .foregroundStyle(Color("Text1")) .font(.title) .padding() Spacer() TextField("Playlist Name", text: $playlistName) .padding() .background(RoundedRectangle(cornerRadius: 10).stroke(Color.white, lineWidth: 1)) .foregroundStyle(Color("Text1")) TextField("M3U Link", text: $playlistUrl) .padding() .background(RoundedRectangle(cornerRadius: 10).stroke(Color.white, lineWidth: 1)) .foregroundStyle(Color("Text1")) .overlay { HStack { Spacer() if !playlistUrl.isEmpty { Button(action: { self.playlistUrl = "" }) { Image(systemName: "multiply.circle.fill") .foregroundColor(.gray) } .padding(.trailing, 8) } } } HStack { Button { Task { await backgroundParser.cancelDownload() isPresented = false } } label: { Text("Return") } .buttonStyle(BorderedButtonStyle()) .foregroundStyle(Color("Text1")) Spacer() Button("Save") { if playlistName != "" && playlistUrl != "" { Task { do { let playlist = PlayListModel(timestamp: Date(), playlistName: playlistName, playlistUrl: playlistUrl, lastUpdate: Date()) try await backgroundParser.downloadData(lista: playlist) { stage, prog, total in print("going tp update \(stage.description), \(prog), \(total)") self.downloadStage = stage self.progress = prog self.total = total if stage == .done { isPresented = false } } } catch { alertMessage = "Error: \(error.localizedDescription)" showAlert = true } } } } .buttonStyle(BorderedButtonStyle()) .foregroundStyle(Color("Text1")) .alert(isPresented: $showAlert) { Alert(title: Text("Error"), message: Text(alertMessage), dismissButton: .default(Text("OK"))) } } .padding(.top, 50) Spacer() VStack{ switch downloadStage { case .downloadData: ProgressView(value: progress, total: CGFloat(total)) { HStack { Text("Downloading Data...").font(.title3) Text("\(Int(progress)) / \(total)") } }.progressViewStyle(.linear) case .parseData: // 此处ProgressView不更新 ProgressView(value: progress, total: 100) { HStack { Text("Parsing Data...").font(.title3) Text("\(Int(progress)) / \(total)") } }.progressViewStyle(.linear) case .savingData: HStack { Text("Saving data...don't close window").font(.title3) } default: Text("") } }.foregroundColor(.white) Spacer() } .padding() .background { LinearGradient(colors: [.color1, .color2, .color3], startPoint: .bottom, endPoint: .topTrailing) .ignoresSafeArea() } } }
原因分析
- 主线程阻塞:
sdparsePlaylist中的for循环是同步密集运算,会持续占用当前线程,导致SwiftUI无法在主线程及时处理@State变量的更新请求,ProgressView因此无法刷新。 - 线程不匹配:下载阶段的进度回调由Alamofire自动切换到主线程执行,而解析阶段的
completion闭包直接在后台线程调用,未切换到主线程更新UI状态,违反了SwiftUI必须在主线程更新UI的规则。
解决方案
1. 强制在主线程更新UI
所有调用completion更新状态的代码,都通过DispatchQueue.main.async切换到主线程:
// 在sdparsePlaylist的循环中修改 let currentProgress = Double(progress) / Double(totalItems) * 100 DispatchQueue.main.async { completion(currentProgress, 100) } // 同时统一downloadData函数中所有completion调用: DispatchQueue.main.async { completion(.parseData, 0, 0) }
2. 给解析循环让出线程执行权
在for循环中加入await Task.yield(),让当前线程暂时让出执行权,给UI更新留出时间:
for item in media { // ... 原有解析代码 ... let currentProgress = Double(progress) / Double(totalItems) * 100 DispatchQueue.main.async { completion(currentProgress, 100) } print("parser background: \(currentProgress)% (\(progress + 1) / \(totalItems))") progress += 1 // 让出线程,允许UI更新 await Task.yield() }
3. 长期优化建议
- 所有耗时解析/运算任务都放在后台线程执行,避免阻塞主线程
- 统一UI更新逻辑,确保所有
@State变量的修改都在主线程完成
内容的提问来源于stack exchange,提问作者Damiano Miazzi
相关产品推荐
相关产品推荐

