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

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()
        }
    }
}

原因分析
  1. 主线程阻塞:sdparsePlaylist中的for循环是同步密集运算,会持续占用当前线程,导致SwiftUI无法在主线程及时处理@State变量的更新请求,ProgressView因此无法刷新。
  2. 线程不匹配:下载阶段的进度回调由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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 23:47:02