使用@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)
}
}
}
切换到主线程的正确位置在哪里?
- 将
@MainActor
分配给fetchUser()
方法:
@MainActor
private func fetchUser() async {
...
}
- 将
@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) {
...
}
}
- 不分配
@MainActor
。在主线程上使用 await MainActor.run
或 Task
和 @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 的地方使用主要演员。话虽如此,在实践中,我发现在主要演员身上也有演示者(或控制器或视图模型或您采用的任何模式)运行 通常是有用的。例如,如果您有同步 UITableViewDataSource
或 UICollectionViewDataSource
方法从演示者那里获取模型数据,则尤其如此。如果你有相关的演示者使用不同的演员,你不能总是 return 同步到数据源。因此,您也可以在主要演员身上使用演示者方法 运行ning。同样,这最好与选项 2 一起考虑,而不是代替它。
所以,简而言之,选项2是谨慎的,但通常与选项1和选项3结合使用。必须 运行 主要演员的例程应该这样指定,而不是将这种负担放在调用者身上。
前面提到的 Swift concurrency: Update a sample app 涵盖了许多这些实际考虑因素,如果您还没有的话,值得一看。
假设您有一个在全局上下文中异步执行的方法。根据执行情况,您需要更新 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)
}
}
}
切换到主线程的正确位置在哪里?
- 将
@MainActor
分配给fetchUser()
方法:
@MainActor
private func fetchUser() async {
...
}
- 将
@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) {
...
}
}
- 不分配
@MainActor
。在主线程上使用await MainActor.run
或Task
和@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 的地方使用主要演员。话虽如此,在实践中,我发现在主要演员身上也有演示者(或控制器或视图模型或您采用的任何模式)运行 通常是有用的。例如,如果您有同步 UITableViewDataSource
或 UICollectionViewDataSource
方法从演示者那里获取模型数据,则尤其如此。如果你有相关的演示者使用不同的演员,你不能总是 return 同步到数据源。因此,您也可以在主要演员身上使用演示者方法 运行ning。同样,这最好与选项 2 一起考虑,而不是代替它。
所以,简而言之,选项2是谨慎的,但通常与选项1和选项3结合使用。必须 运行 主要演员的例程应该这样指定,而不是将这种负担放在调用者身上。
前面提到的 Swift concurrency: Update a sample app 涵盖了许多这些实际考虑因素,如果您还没有的话,值得一看。