异步代码的Task {}应在ViewModel还是ViewController中使用?
Swift异步代码:Task包装位置与async函数的最佳实践
结论先行
将ViewModel的函数标记为async,然后在ViewController的Task代码块中调用,这是完全可行且符合Swift Concurrency设计理念的标准方案,比在ViewModel里包装Task的方式更简洁、易维护。
两种方案的优劣对比
1. ViewModel内部用Task {…}包装异步逻辑
这种做法会让ViewModel的对外接口变成同步函数,但存在明显弊端:
- 浪费了Swift Concurrency的结构化并发优势,没法直接用
async/await的简洁语法 - 必须依赖回调、代理、RxSwift等传统方式传递异步结果,代码冗余且容易陷入回调地狱
- 调用方无法从函数签名直观判断这是异步操作,增加了理解和维护成本
2. ViewModel函数标记为async,在ViewController的Task中调用
这是当前Swift异步编程的推荐模式,优势非常明显:
- 函数签名清晰,
async关键字直接表明这是一个需要等待的异步操作 - 无需额外的回调或代理,通过
await就能获取结果,代码结构更扁平 - 可以利用结构化并发的特性,比如任务取消、错误处理,同时能方便地切回主队列更新UI
代码示例
ViewModel实现
class DataViewModel { // 明确标记为async函数,暴露异步逻辑 func fetchUserData() async throws -> [User] { let url = URL(string: "https://api.example.com/users")! let (data, response) = try await URLSession.shared.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NetworkError.invalidResponse } return try JSONDecoder().decode([User].self, from: data) } } enum NetworkError: Error { case invalidResponse } struct User: Codable { let id: Int let name: String }
ViewController调用
class UserListViewController: UIViewController { private let viewModel = DataViewModel() private var users: [User] = [] private let tableView = UITableView() override func viewDidLoad() { super.viewDidLoad() setupTableView() fetchUsers() } private func fetchUsers() { // 在Task中调用ViewModel的async函数 Task { do { users = try await viewModel.fetchUserData() // 切回主队列更新UI await MainActor.run { tableView.reloadData() } } catch { await MainActor.run { showError(message: error.localizedDescription) } } } } // 其他UI相关方法... private func setupTableView() { tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UserCell") tableView.dataSource = self tableView.frame = view.bounds view.addSubview(tableView) } private func showError(message: String) { let alert = UIAlertController(title: "出错了", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default)) present(alert, animated: true) } } // UITableViewDataSource实现... extension UserListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { users.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) cell.textLabel?.text = users[indexPath.row].name return cell } }
额外提示
- 如果你的ViewController被标记为
@MainActor,那么Task中的代码默认运行在主队列,更新UI时可以省去MainActor.run的调用 - 记得处理任务取消:比如在ViewController的
viewWillDisappear中取消对应的Task,避免内存泄漏或无效的网络请求
内容的提问来源于stack exchange,提问作者Roman
相关产品推荐
相关产品推荐

