Матрёшка из Task внутри ViewModel
Есть популярный паттерн, который выглядит знакомым многим SwiftUI разработчикам:
@MainActor
final class ArticleViewModel: ObservableObject {
@Published var isPublishing = false
func publish(_ article: Article) {
isPublishing = true
Task {
await ArticlePublisher.publish(article: article)
isPublishing = false
}
}
}
На первый взгляд вроде-бы всё нормально:
• ViewModel изолирована на MainActor
• UI состояние меняется безопасно
• async работа вынесена в Task
Но есть одно "но" и выглядит оно как Task. Кто-то сразу прикинул:"Ага, ну тут у нас просто Task, а не Task.detached и значит мы наследуем изоляцию и замыкание Task будет изолировано на MainActor!" И будет прав, достаточно типичный пример в SwiftUI разработке.
Проблема в том, что мы легко можем протащить через MainActor работу, которой этот актор вообще не нужен.
Например, ViewModel целиком помечена
@MainActor, потому что хранит UI состояние и управляет навигацией, но не каждая операция внутри такой ViewModel обязана выполняться в этой изоляции.
Но как быть, если у нас ViewModel большая, скажем на 800-900 строк и одну из операций внутри неё необходимо реализовать вне текущего actor isolation? 🤔
В Swift 6.2 для такого случая есть явный инструмент - атрибут
@concurrent.
Task {
@concurrent in
await ArticlePublisher.publish(article: article)
await MainActor.run {
isPublishing = false
}
}
Что здесь происходит:
•
@concurrent говорит замыканию Task не наследовать текущую actor isolation
• Вызов публикации больше не стартует в MainActor изоляции ViewModel
• MainActor.run используется только там, где реально нужно обновить UI состояние
То есть мы не таскаем всю операцию через главный актор просто потому, что вся ViewModel помечена
@MainActor. И
@concurrent нужен как раз для случаев, когда мы хотим явно сказать:
Эту async работу не нужно выполнять в текущей actor isolation
Тем самым ближе поз