如何在代码执行过程中更新SwiftUI界面向用户反馈运行进度?
核心原因
SwiftUI的所有UI渲染操作都运行在主线程,你之前的方案之所以看不到中间进度更新,是因为耗时任务直接在主线程同步执行,阻塞了主线程的UI刷新队列。所有对Alert文本、Text内容的修改都会被暂时压入队列,等耗时任务执行完成后才会统一批量渲染,因此只能看到最终状态。
实现方案
首先你需要把耗时的计算、IO等任务移到非主线程执行,仅在需要更新进度时切回主线程修改状态变量,就能实现UI的实时刷新。
方案1:Sheet弹窗展示进度(推荐)
系统Alert的设计定位是轻量提示,不适合频繁更新内容,用Sheet自定义进度弹窗体验更好,示例代码如下:
视图模型代码
import SwiftUI @MainActor class ProgressViewModel: ObservableObject { @Published var showProgress = false @Published var progressText = "准备中..." @Published var progressValue: Double = 0.0 // 有明确进度时使用 func runHeavyTask() async { showProgress = true let totalStep = 10 // 替换为你的任务总步数 for i in 0..<totalStep { // 替换为你的实际业务逻辑 try? await Task.sleep(nanoseconds: 500_000_000) // 因ViewModel标记了@MainActor,直接修改状态即可自动切到主线程 progressText = "正在处理第\(i+1)项,共\(totalStep)项" progressValue = Double(i+1)/Double(totalStep) } progressText = "处理完成" try? await Task.sleep(nanoseconds: 1_000_000_000) showProgress = false } }
视图层代码
struct ContentView: View { @StateObject private var vm = ProgressViewModel() var body: some View { Button("启动任务") { Task { await vm.runHeavyTask() } } .sheet(isPresented: $vm.showProgress) { VStack(spacing: 20) { ProgressView(value: vm.progressValue) { Text(vm.progressText) } currentValueLabel: { Text(String(format: "%.0f%%", vm.progressValue * 100)) } .padding() .interactiveDismissDisabled() // 禁止用户手动关闭弹窗 } .frame(width: 300, height: 150) } } }
方案2:Alert实现动态进度(不推荐)
如果一定要用Alert实现,只要保证耗时任务在后台执行,绑定Alert的message到动态状态变量即可实时更新:
struct AlertProgressDemo: View { @State private var showAlert = false @State private var alertMessage = "准备中..." var body: some View { Button("启动任务") { showAlert = true // 后台执行耗时任务 Task.detached { let totalStep = 10 for i in 0..<totalStep { // 替换为实际业务逻辑 try? await Task.sleep(nanoseconds: 500_000_000) // 更新UI必须切回主线程 await MainActor.run { alertMessage = "正在处理第\(i+1)项" } } await MainActor.run { alertMessage = "处理完成" // 延迟1秒关闭弹窗 Task { try? await Task.sleep(nanoseconds: 1_000_000_000) showAlert = false } } } } .alert("处理中", isPresented: $showAlert) { Button("取消", role: .cancel) { // 可添加任务取消逻辑 } } message: { Text(alertMessage) } } }
兼容老代码的GCD实现方案
如果你的项目还没有使用Swift Concurrency,可以用GCD实现同样的效果:
// 按钮点击回调 Button("启动任务") { showProgress = true // 把耗时任务丢到全局后台队列 DispatchQueue.global().async { let totalStep = 10 for i in 0..<totalStep { // 模拟耗时操作 Thread.sleep(forTimeInterval: 0.5) // 进度更新切回主队列 DispatchQueue.main.async { progressText = "正在处理第\(i+1)项" progressValue = Double(i+1)/Double(totalStep) } } DispatchQueue.main.async { progressText = "处理完成" DispatchQueue.main.asyncAfter(deadline: .now() + 1) { showProgress = false } } } }
注意事项
- 所有UI相关的状态修改必须在主线程执行,否则会出现渲染异常甚至崩溃
- 耗时任务绝对不能放在主线程同步执行,否则一定会阻塞UI刷新
- 可以给耗时任务添加取消逻辑,用户点击取消按钮时终止正在执行的后台任务
内容的提问来源于stack exchange,提问作者Kyra
相关产品推荐
相关产品推荐

