.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 робить це автоматично, що зменшує кількість шаблонного коду та виключає ризик забути скасувати завдання.
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. Це безпечніше та чистіше з точки зору архітектури.
// ❌ Застарілий підхід: Task у .onAppear без скасування
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
.onAppear {
loadTask = Task { await loadData() }
}
.onDisappear { loadTask?.cancel() }
// ✅ Сучасний підхід: .task керує скасуванням
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 {
// обробити помилку
}
}
}
Коли categoryId змінюється, SwiftUI скасовує попередній запит і запускає новий. Це особливо важливо при швидких перемиканнях категорій — старі запити не будуть конкурувати з новими за оновлення стану. Без .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 {
// Перевірити скасування
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("Не вдалося завантажити")
}
}
.task {
defer { isLoading = false }
do {
profile = await APIClient().fetchProfile()
} catch {
// профіль залишається nil
}
}
}
}
struct DashboardView: View {
@State var stats: DashboardStats?
var body: some View {
Text("Панель керування")
.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.
// ❌ Помилка: без обробки помилок
.task {
let data = await fetchData() // падає при помилці!
items = data
}
// ✅ Правильно: 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), воно продовжить виконуватися, але його результат не буде застосовано до стану, оскільки View уже не існує.
Так, можна додати кілька .task модифікаторів на одне View. Кожен створює незалежне завдання. Це зручно для розділення різних джерел даних: один .task для завантаження профілю, другий для підписки на WebSocket, третій для моніторингу геолокації.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також