Administrator
发布于 2026-02-24 / 1 阅读
0
0

Task/Async/Await

你这个感觉非常对

View 里出现 await presenter.refresh(),哪怕只是一行,也确实像在“指挥业务”。

但这里要分清两层:

  • View 触发一个用户意图(Intent):这是 UI 职责(不算业务)

  • View 组织业务流程(Workflow):这才是业务

presenter.refresh() 如果只是“响应点击的意图”,其实还在 UI 职责边界内;

但你想更纯粹:View 连“refresh”这个业务词都不出现,只说“用户点了按钮”,这就进入 MVI/Intent 风格 了。

我给你三种越来越“干净”的做法,你可以按你想要的纯度选。


方案 A(你现在的):View 调用 Presenter 的用例方法

.anyAsyncButton { await presenter.refresh() }

✅ 简单、清晰

✅ SwiftUI 常见

⚠️ View 仍然知道 “refresh” 这个业务名


方案 B(更纯):View 只发送“意图”,Presenter 自己决定做什么

把 refresh() 换成 send(_:):

enum SettingsIntent {
  case refreshTapped
  case signOutTapped
  case deleteAccountTapped
}

View:

.anyButton {
  presenter.send(.signOutTapped)
}

Presenter:

@MainActor
func send(_ intent: SettingsIntent) {
  switch intent {
  case .signOutTapped:
    task = Task { await signOut() }
  case .deleteAccountTapped:
    task = Task { await deleteAccount() }
  case .refreshTapped:
    task = Task { await refresh() }
  }
}

✅ View 完全不碰 await

✅ View 只描述“用户做了什么”

✅ 业务流程集中在 Presenter

⚠️ Presenter 内部会用 Task(但这是“业务编排层”,合理)

⚠️ 需要管理 task 去重/取消(我下面给你更优雅的)

你担心 “func 内部 Task 不推荐”——这里属于 UI 意图入口层,它本来就是 async 的边界之一,允许用 Task。


方案 C(最纯 + 最稳):View 只改状态,Presenter 通过状态变化触发异步(Optional 驱动)

这是你之前提到的“Presenter 一定要 Optional 驱动”的落地版。

View 不调用任何方法,甚至不发 intent,只改一个状态:

Button("Sign Out") {
  presenter.intent = .signOut
}

Presenter:

enum Intent: Equatable { case signOut, deleteAccount, refresh }

@Observable
final class SettingsPresenter {
  var intent: Intent? = nil
  var alert: AnyAppAlert? = nil

  func bind() {
    // 伪代码:监听 intent 变化触发任务
  }
}

SwiftUI 里更常见做法是:View 调用 presenter.handle(.signOut)(方案 B),而不是做复杂监听;但如果你在做更“声明式”的架构,这条路可以走到极致。

✅ View 极度纯

⚠️ 实现复杂,收益要看项目规模


你现在最适合哪一种?(按 AIChats 规模)

我建议你用 方案 BIntent + Presenter 内部 Task 管理

原因:

  • 你已经有 AnyAppAlert、AppState、FirebaseAuthService

  • 你希望 View 不写业务

  • 你同时又希望并发结构清晰、可取消、可去重


方案 B 的“最佳实践版本”:Presenter 自己管理任务(防连点 + 可取消)

@MainActor
final class SettingsPresenter: ObservableObject {
  @Published var alert: AnyAppAlert?
  @Published var showCreateAccountView = false

  private var runningTask: Task<Void, Never>?

  func send(_ intent: Intent) {
    runningTask?.cancel()
    runningTask = Task { [weak self] in
      guard let self else { return }
      await self.handle(intent)
    }
  }

  enum Intent {
    case signOutTapped
    case deleteAccountTapped
    case createAccountTapped
  }

  private func handle(_ intent: Intent) async {
    switch intent {
    case .createAccountTapped:
      showCreateAccountView = true

    case .signOutTapped:
      // do/try/catch -> update appState/alert

    case .deleteAccountTapped:
      // do/try/catch -> update appState/alert
    }
  }
}

View 变成:

Text("Sign Out")
  .anyButton { presenter.send(.signOutTapped) }

Text("Delete Account")
  .anyButton { presenter.send(.deleteAccountTapped) }

✅ View 完全不 await

✅ View 不提业务函数名

✅ 业务/并发/错误集中在 Presenter

✅ 防重复点击、支持取消


最关键的一点:你担心“在 func 里用 Task”会破坏 structured concurrency

这个担心是对的,但要注意:

structured concurrency 的边界一定存在

(比如按钮点击、通知回调、delegate 回调、timer)

这些地方天生是同步入口,你必须用 Task 桥接。

所以“Task 只能在边界用一次”——

边界不一定是 View,也可以是 Presenter 的 intent 入口。

只要你把 Task 统一放在 Presenter 的 send(intent:) 入口里,任务管理集中,反而更干净、更可控。


如果你愿意,我可以直接帮你把 SettingsView 按方案 B 重构成:

  • View 0 业务词

  • Presenter 统一处理 async

  • 错误统一写到 AppState.alert

  • 并且自带防抖/去重/取消

你贴一下 AppState 结构(尤其 updateAppState 和 alert 存在哪里),我就能给你一份可以直接落地的版本。


评论