async/await 最容易被误用的地方,不是语法,而是边界。业务层如果随手创建 Task,很快会出现取消失效、重复请求、UI 状态被旧响应覆盖的问题。
把生命周期交给调用方
ViewController 或 ViewModel 应该持有任务引用,并在页面消失、条件变化或新请求开始时取消旧任务。网络层只负责响应取消,不应该替 UI 猜生命周期。
final class ProfileViewModel {
private var loadingTask: Task<Void, Never>?
func load(userID: String) {
loadingTask?.cancel()
loadingTask = Task { [weak self] in
do {
let profile = try await self?.service.profile(id: userID)
try Task.checkCancellation()
await MainActor.run { self?.state = .loaded(profile) }
} catch is CancellationError {
return
} catch {
await MainActor.run { self?.state = .failed(error) }
}
}
}
}
主线程边界必须显式
业务层可以在后台 await 网络和解析,但状态写入、UIKit 调用和可观察属性更新必须回到 MainActor。不要依赖“现在看起来没问题”。
错误要被翻译
底层错误不应该直接冲到 UI。把网络错误、业务错误和取消分开,UI 才能展示明确状态,而不是把所有失败都当成同一个 toast。