.task { } — это модификатор в SwiftUI, представленный в iOS 15, который запускает асинхронную операцию при появлении View и автоматически отменяет её при исчезновении. В отличие от .onAppear, который выполняет синхронный код без возможности отмены, .task работает с async/await контекстом и учитывает жизненный цикл View: при исчезновении View SwiftUI вызывает cancel() на созданном Task. Это предотвращает утечки памяти и выполнение операций после того, как View уже не нужно обновлять. По данным Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024), .task является предпочтительным способом загрузки данных в SwiftUI, поскольку он безопасно работает со Structured Concurrency и автоматически управляет временем жизни асинхронной операции.
Главное
.task { } — это модификатор View, который создаёт Task в async-контексте при появлении View на экране. SwiftUI запускает переданное замыкание в фоновом потоке, при этом главный поток остаётся свободным для UI-операций. Когда View исчезает, SwiftUI автоматически отменяет Task через механизм Structured Concurrency — это гарантирует, что асинхронная операция не продолжит выполняться после того, как её результат уже никому не нужен.
По данным Apple — Swift Programming Language (2025), .task использует концепцию Structured Concurrency, введённую в Swift 5.5. Каждый .task создаёт дочернюю задачу в рамках задачи родительского View. Если родительская задача отменяется (View исчезает), все дочерние задачи также отменяются автоматически. Это радикально упрощает управление жизненным циклом асинхронных операций по сравнению с ручным хранением ссылок на DispatchWorkItem или AnyCancellable.
В отличие от традиционного подхода с @State + ручным вызовом в .onAppear, .task не требует хранения ссылки на Task для последующей отмены. SwiftUI делает это автоматически, что уменьшает количество boilerplate кода и исключает риск забыть отменить задачу.
struct ArticlesView: View {
@State var articles: [Article] = []
@State var error: Error?
var body: some View {
List(articles) { article in
Text(article.title)
}
.task {
do {
articles = await APIClient().fetchArticles()
} catch {
self.error = error
}
}
}
}
Многие разработчики привыкли загружать данные в .onAppear, но с появлением async/await и .task этот подход устарел. .onAppear выполняет код синхронно — для асинхронных операций внутри .onAppear нужно оборачивать вызов в Task { } и вручную сохранять ссылку на него для возможной отмены. .task делает это автоматически.
| Характеристика | .task { } | .onAppear |
|---|---|---|
| Async контекст | Встроенный async/await | Требует Task { } обёртки |
| Автоотмена | Да, при исчезновении View | Нет, нужно реализовать вручную |
| Structured Concurrency | Поддерживает | Не поддерживает |
| Повторный запуск | Только при изменении id | Каждый раз при появлении |
| Рекомендация Apple | Предпочтительный способ | Для синхронных операций |
.onAppear всё ещё полезен для синхронных операций — например, логирования или начальной настройки UI. Но для асинхронной загрузки данных, сетевых запросов, работы с базой данных или файловой системой используй .task. Это безопаснее и чище с точки зрения архитектуры.
// ❌ Legacy approach: Task in .onAppear without cancellation
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
.onAppear {
loadTask = Task { await loadData() }
}
.onDisappear { loadTask?.cancel() }
// ✅ Modern approach: .task manages cancellation
func body() { var body: some View { Text("") }
.task { await loadData() }
Модификатор .task(id:) принимает дополнительный параметр — идентификатор. Когда значение идентификатора изменяется, SwiftUI отменяет текущую задачу и запускает новую с новым идентификатором. Это идеально подходит для экранов, где данные зависят от выбранного параметра — например, список статей по категории или детали товара по ID.
struct CategoryView: View {
let categoryId: Int
@State var items: [Item] = []
var body: some View {
List(items) { item in
Text(item.name)
}
.task(id: categoryId) {
await loadItems(for: categoryId)
}
}
func loadItems(for id: Int) async {
do {
items = await APIClient().fetchItems(categoryId: id)
} catch {
// handle error
}
}
}
Когда categoryId изменяется, SwiftUI отменяет предыдущий запрос и запускает новый. Это особенно важно при быстрых переключениях категорий — старые запросы не будут конкурировать с новыми за обновление state. Без .task(id:) пришлось бы вручную отслеживать изменения через .onChange и управлять Task вручную.
Хотя .task автоматически отменяет задачу при исчезновении View, сама асинхронная операция должна кооперативно проверять отмену. Swift использует кооперативную модель отмены — Task.cancel() не останавливает выполнение принудительно, а лишь устанавливает флаг isCancelled. Код внутри задачи должен периодически проверять этот флаг.
struct LoadingView: View {
@State var progress: Double = 0
var body: some View {
ProgressView(value: progress)
.task {
for i in 0..<100 {
// Check cancellation
try Task.checkCancellation()
await Task.sleep(nanoseconds: 50_000_000)
progress = Double(i + 1) / 100.0
}
}
}
}
Task.checkCancellation() выбрасывает CancellationError, если задача была отменена. Это самый простой способ проверки — он работает в любом async-контексте. Альтернатива — проверять Task.isCancelled вручную перед затратными операциями. Для URLSession сетевые запросы автоматически отменяются при отмене задачи, так как URLSession поддерживает Structured Concurrency из коробки.
.task подходит для множества сценариев: от простой загрузки JSON до сложных параллельных операций с TaskGroup. Рассмотрим три типичных примера использования.
struct ProfileView: View {
@State var profile: Profile?
@State var isLoading = true
var body: some View {
Group {
if isLoading {
ProgressView()
} else if let profile {
Text(profile.name)
} else {
Text("Failed to load")
}
}
.task {
defer { isLoading = false }
do {
profile = await APIClient().fetchProfile()
} catch {
// profile stays nil
}
}
}
}
struct DashboardView: View {
@State var stats: DashboardStats?
var body: some View {
Text("Dashboard")
.task {
stats = await Task {
await withThrowingTaskGroup { group in
group.addTask { await API().fetchUsers() }
group.addTask { await API().fetchOrders() }
group.addTask { await API().fetchRevenue() }
return DashboardStats(
users: try await group.next(),
orders: try await group.next(),
revenue: try await group.next()
)
}
}.value
}
}
}
Наиболее распространённая ошибка — мутация UI-свойств внутри .task без переключения на главный поток. Хотя SwiftUI автоматически возвращает обновления на главный поток при изменении @State в async-контексте, прямые манипуляции с UIKit-элементами внутри .task могут вызвать crash.
// ❌ Error: no error handling
.task {
let data = await fetchData() // crashes on error!
items = data
}
// ✅ Correct: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
Часто задаваемые вопросы
.task автоматически отменяет задачу при исчезновении View и поддерживает Structured Concurrency. Task { } в .onAppear требует ручного хранения ссылки на задачу и вызова cancel() в .onDisappear. .task также проще читается — он явно указывает, что загрузка данных является частью жизненного цикла View.
Да, для подписки на AsyncSequence или AsyncStream используй for await value in publisher.values внутри .task. Это работает как с async/await, так и с Combine через расширение Publisher.values. При исчезновении View итерация автоматически завершится, и подписка будет отменена.
Да, если View внутри TabView пересоздаётся при каждом переключении. Начиная с iOS 18 TabView может сохранять View в памяти — в этом случае .task не запускается повторно. Используй .task(id:) с идентификатором вкладки, если нужно перезагружать данные при каждом переключении.
SwiftUI отменит задачу при исчезновении View. Если URLSession запрос был внутри задачи, он также будет отменён. Если задача не поддерживает кооперативную отмену (например, не проверяет isCancelled), она продолжит выполняться, но её результат не будет применён к state, так как View уже не существует.
Да, можно добавить несколько .task модификаторов на одно View. Каждый создаёт независимую задачу. Это удобно для разделения разных источников данных: один .task для загрузки профиля, второй для подписки на WebSocket, третий для мониторинга геолокации.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также