.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 стартира подаденото closure във фонов режим, докато основната нишка остава свободна за 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. Това е по-безопасно и по-чисто от архитектурна гледна точка.
// ❌ Стар подход: 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 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също