使用@MainActor 更新UI 的合适策略是什么?

What is the appropriate strategy for using @MainActor to update UI?

假设您有一个在全局上下文中异步执行的方法。根据执行情况,您需要更新 UI.

private func fetchUser() async {
    do {
        let user = try await authService.fetchCurrentUser()
        view.setUser(user)
    } catch {
        if let error = error {
            view.showError(message: error.message)
        }
    }
}

切换到主线程的正确位置在哪里?

  1. @MainActor分配给fetchUser()方法:
@MainActor
private func fetchUser() async { 
    ...
}
  1. @MainActor 分配给 setUser(_ user: User)showError(message: String) 视图的方法:
class SomePresenter {

    private func fetchUser() async {
        do {
            let user = try await authService.fetchCurrentUser()
            await view.setUser(user)
        } catch {
            if let error = error {
                await view.showError(message: error.message)
            }
        }
    }

}

class SomeViewController: UIViewController {

    @MainActor
    func setUser(_ user: User) {
        ...
    }

    @MainActor
    func showError(message: String) {
        ...
    }

}
  1. 不分配 @MainActor。在主线程上使用 await MainActor.runTask@MainActor 代替 运行 setUser(_ user: User)showError(message: String)(如 DispatchQueue.main.async):
private func fetchUser() async {
    do {
        let user = try await authService.fetchCurrentUser()
        
        await MainActor.run {
            view.setUser(user)
        }
    } catch {
        if let error = error {
            await MainActor.run {
                view.showError(message: error.message)
            }
        }
    }
}

选项 2 是合乎逻辑的,因为您让必须 运行 在主队列上的函数声明自己。然后,如果您错误地调用它们,编译器会警告您。更简单的是,您可以将具有这些函数的 class 声明为 @MainActor 本身,然后您就不必像这样声明各个函数。例如,因为 well-designed 视图或视图控制器将自身限制为仅 view-related 代码,所以将整个 class 声明为 @MainActor 并完成它是安全的。

选项 3(代替选项 2)很脆弱,要求应用程序开发人员必须记住在主要角色上手动 run 它们。如果您没有做正确的事情,您将失去 compile-time 警告。 Compile-time 警告总是好的。但是 WWDC 2021 视频 Swift concurrency: Update a sample app 指出,即使你采用选项 2,如果你需要调用一系列 MainActor 方法,你可能仍然会使用 MainActor.run 并且你可能不想招致await 一个又一个调用的开销,而是将主要参与者函数组包装在一个 MainActor.run 块中。 (但您可能仍会考虑将此与选项 2 结合使用,而不是代替它。)

在摘要中,选项 1 可以说有点 heavy-handed,指定的功能不一定要 运行 对主要参与者执行此操作。您应该只在明确 needed/desired 的地方使用主要演员。话虽如此,在实践中,我发现在主要演员身上也有演示者(或控制器或视图模型或您采用的任何模式)运行 通常是有用的。例如,如果您有同步 UITableViewDataSourceUICollectionViewDataSource 方法从演示者那里获取模型数据,则尤其如此。如果你有相关的演示者使用不同的演员,你不能总是 return 同步到数据源。因此,您也可以在主要演员身上使用演示者方法 运行ning。同样,这最好与选项 2 一起考虑,而不是代替它。

所以,简而言之,选项2是谨慎的,但通常与选项1和选项3结合使用。必须 运行 主要演员的例程应该这样指定,而不是将这种负担放在调用者身上。


前面提到的 Swift concurrency: Update a sample app 涵盖了许多这些实际考虑因素,如果您还没有的话,值得一看。