SwiftUI macOS应用切换视图后@State变量状态丢失如何解决?
解决方案
1. 把状态和任务管理移到可观察对象中
@State是视图本地状态,当视图切换再返回时,视图实例可能被重新初始化,导致busy重置,但之前的下载任务还在后台运行。要解决这个问题,需要把状态和任务逻辑抽离到ObservableObject中,用@StateObject持有它——这个属性的生命周期和视图绑定,即使视图重建,对象也不会被重新初始化,状态能一直保留。
示例代码:
import SwiftUI class DownloadManager: ObservableObject { @Published var isBusy = false private var downloadTask: Task<Void, Error>? func startDownload(for content: Content) { // 避免重复触发同一下载 guard !isBusy else { return } isBusy = true downloadTask = Task { [weak self] in do { try await content.download() } catch { // 这里可以添加错误处理,比如弹窗提示用户 print("下载失败: \(error)") } // 任务被取消时,不要更新状态 guard !Task.isCancelled else { return } self?.isBusy = false } } // 可选:添加取消下载的功能 func cancelDownload() { downloadTask?.cancel() downloadTask = nil isBusy = false } } struct DownloadRow: View { let content: Content @StateObject private var downloadManager = DownloadManager() var body: some View { if !downloadManager.isBusy { Button(action: { downloadManager.startDownload(for: content) }) { Label("Download") } } else { HStack { ProgressView() // 可选:显示取消按钮 Button("Cancel") { downloadManager.cancelDownload() } } } } }
2. 关键细节说明
@StateObject的作用:专门用于持有ObservableObject实例,确保视图重建时对象不会被销毁,状态得以保留。- 任务实例管理:用属性保存
Task实例,既可以防止重复触发下载,也能支持后续的取消操作。 - 弱引用
self:在Task闭包中使用[weak self],避免循环引用导致内存泄漏。 - 任务取消检查:通过
Task.isCancelled判断任务是否被取消,避免在任务取消后错误地更新状态。
3. 多内容场景优化
如果你的界面有多个DownloadRow对应不同的Content,可以修改DownloadManager为每个Content单独维护下载状态,比如用字典存储任务和状态:
class DownloadManager: ObservableObject { @Published var busyContentIDs: Set<Content.ID> = [] private var tasks: [Content.ID: Task<Void, Error>] = [:] func startDownload(for content: Content) { let id = content.id guard !busyContentIDs.contains(id) else { return } busyContentIDs.insert(id) tasks[id] = Task { [weak self] in do { try await content.download() } catch { print("下载失败: \(error)") } guard !Task.isCancelled else { return } self?.busyContentIDs.remove(id) self?.tasks.removeValue(forKey: id) } } func cancelDownload(for content: Content) { let id = content.id tasks[id]?.cancel() tasks.removeValue(forKey: id) busyContentIDs.remove(id) } } // 假设你的Content类型有唯一ID属性 protocol Content { var id: UUID { get } func download() async throws } struct DownloadRow: View { let content: Content @StateObject private var downloadManager = DownloadManager() var body: some View { let isBusy = downloadManager.busyContentIDs.contains(content.id) if !isBusy { Button(action: { downloadManager.startDownload(for: content) }) { Label("Download") } } else { HStack { ProgressView() Button("Cancel") { downloadManager.cancelDownload(for: content) } } } } }
这样每个Content的下载状态都能独立管理,即使切换视图再返回,所有正在下载的任务状态都能正确显示。
内容的提问来源于stack exchange,提问作者Milan
相关产品推荐
相关产品推荐

