.task { } — to modyfikator w SwiftUI, wprowadzony w iOS 15, który uruchamia asynchroniczną operację przy pojawieniu się View i automatycznie ją anuluje przy zniknięciu. W przeciwieństwie do .onAppear, który wykonuje synchroniczny kod bez możliwości anulowania, .task działa w kontekście async/await i uwzględnia cykl życia View: przy zniknięciu View SwiftUI wywołuje cancel() na utworzonym Task. Zapobiega to wyciekom pamięci i wykonywaniu operacji po tym, jak View nie wymaga już aktualizacji. Według Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024), .task jest preferowanym sposobem ładowania danych w SwiftUI, ponieważ bezpiecznie współpracuje ze Structured Concurrency i automatycznie zarządza czasem życia operacji asynchronicznej.
Najważniejsze
.task { } — to modyfikator View, który tworzy Task w kontekście async przy pojawieniu się View na ekranie. SwiftUI uruchamia przekazane zamknięcie w tle, podczas gdy główny wątek pozostaje wolny dla operacji UI. Gdy View znika, SwiftUI automatycznie anuluje Task poprzez mechanizm Structured Concurrency — gwarantuje to, że operacja asynchroniczna nie będzie kontynuowana po tym, jak jej wynik nie jest już nikomu potrzebny.
Według Apple — Swift Programming Language (2025), .task używa koncepcji Structured Concurrency, wprowadzonej w Swift 5.5. Każdy .task tworzy zadanie podrzędne w ramach zadania nadrzędnego View. Jeśli zadanie nadrzędne zostanie anulowane (View znika), wszystkie zadania podrzędne również są automatycznie anulowane. To radykalnie upraszcza zarządzanie cyklem życia operacji asynchronicznych w porównaniu z ręcznym przechowywaniem referencji do DispatchWorkItem lub AnyCancellable.
W przeciwieństwie do tradycyjnego podejścia z @State + ręcznym wywołaniem w .onAppear, .task nie wymaga przechowywania referencji do Task w celu późniejszego anulowania. SwiftUI robi to automatycznie, co zmniejsza ilość kodu boilerplate i eliminuje ryzyko zapomnienia o anulowaniu zadania.
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
}
}
}
}
Wielu programistów przyzwyczaiło się ładować dane w .onAppear, ale wraz z pojawieniem się async/await i .task to podejście stało się przestarzałe. .onAppear wykonuje kod synchronicznie — dla operacji asynchronicznych wewnątrz .onAppear trzeba opakować wywołanie w Task { } i ręcznie przechowywać referencję do niego w celu ewentualnego anulowania. .task robi to automatycznie.
| Cecha | .task { } | .onAppear |
|---|---|---|
| Kontekst async | Wbudowany async/await | Wymaga opakowania Task { } |
| Autoanulowanie | Tak, przy zniknięciu View | Nie, trzeba zaimplementować ręcznie |
| Structured Concurrency | Obsługuje | Nie obsługuje |
| Ponowne uruchomienie | Tylko przy zmianie id | Za każdym razem przy pojawieniu się |
| Zalecenie Apple | Preferowany sposób | Dla operacji synchronicznych |
.onAppear wciąż jest przydatny dla operacji synchronicznych — na przykład logowania lub początkowej konfiguracji UI. Ale do asynchronicznego ładowania danych, zapytań sieciowych, pracy z bazą danych lub systemem plików używaj .task. Jest to bezpieczniejsze i czystsze z punktu widzenia architektury.
// ❌ Starsze podejście: Task w .onAppear bez anulowania
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
.onAppear {
loadTask = Task { await loadData() }
}
.onDisappear { loadTask?.cancel() }
// ✅ Nowoczesne podejście: .task zarządza anulowaniem
func body() { var body: some View { Text("") }
.task { await loadData() }
Modyfikator .task(id:) przyjmuje dodatkowy parametr — identyfikator. Gdy wartość identyfikatora się zmienia, SwiftUI anuluje bieżące zadanie i uruchamia nowe z nowym identyfikatorem. Jest to idealne rozwiązanie dla ekranów, gdzie dane zależą od wybranego parametru — na przykład lista artykułów według kategorii lub szczegóły produktu według 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 {
// obsłuż błąd
}
}
}
Gdy categoryId się zmienia, SwiftUI anuluje poprzednie zapytanie i uruchamia nowe. Jest to szczególnie ważne przy szybkim przełączaniu kategorii — stare zapytania nie będą konkurować z nowymi o aktualizację stanu. Bez .task(id:) trzeba by ręcznie śledzić zmiany przez .onChange i ręcznie zarządzać Task.
Chociaż .task automatycznie anuluje zadanie przy zniknięciu View, sama operacja asynchroniczna powinna kooperatywnie sprawdzać anulowanie. Swift używa kooperatywnego modelu anulowania — Task.cancel() nie zatrzymuje wykonania przymusowo, a jedynie ustawia flagę isCancelled. Kod wewnątrz zadania powinien okresowo sprawdzać tę flagę.
struct LoadingView: View {
@State var progress: Double = 0
var body: some View {
ProgressView(value: progress)
.task {
for i in 0..<100 {
// Sprawdź anulowanie
try Task.checkCancellation()
await Task.sleep(nanoseconds: 50_000_000)
progress = Double(i + 1) / 100.0
}
}
}
}
Task.checkCancellation() rzuca CancellationError, jeśli zadanie zostało anulowane. To najprostszy sposób sprawdzenia — działa w każdym kontekście async. Alternatywa — ręczne sprawdzanie Task.isCancelled przed kosztownymi operacjami. Dla URLSession zapytania sieciowe są automatycznie anulowane przy anulowaniu zadania, ponieważ URLSession obsługuje Structured Concurrency od razu.
.task nadaje się do wielu scenariuszy: od prostego ładowania JSON po złożone operacje równoległe z TaskGroup. Rozważmy trzy typowe przykłady użycia.
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("Nie udało się załadować")
}
}
.task {
defer { isLoading = false }
do {
profile = await APIClient().fetchProfile()
} catch {
// profil pozostaje nil
}
}
}
}
struct DashboardView: View {
@State var stats: DashboardStats?
var body: some View {
Text("Panel główny")
.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
}
}
}
Najczęstszym błędem jest mutacja właściwości UI wewnątrz .task bez przełączania na główny wątek. Chociaż SwiftUI automatycznie zwraca aktualizacje na główny wątek przy zmianie @State w kontekście async, bezpośrednie manipulacje elementami UIKit wewnątrz .task mogą spowodować crash.
// ❌ Błąd: brak obsługi błędów
.task {
let data = await fetchData() // awaria przy błędzie!
items = data
}
// ✅ Prawidłowo: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
Często zadawane pytania
.task automatycznie anuluje zadanie przy zniknięciu View i obsługuje Structured Concurrency. Task { } w .onAppear wymaga ręcznego przechowywania referencji do zadania i wywołania cancel() w .onDisappear. .task jest również łatwiejszy do czytania — wyraźnie wskazuje, że ładowanie danych jest częścią cyklu życia View.
Tak, do subskrypcji AsyncSequence lub AsyncStream użyj for await value in publisher.values wewnątrz .task. Działa to zarówno z async/await, jak i z Combine przez rozszerzenie Publisher.values. Przy zniknięciu View iteracja automatycznie się zakończy, a subskrypcja zostanie anulowana.
Tak, jeśli View wewnątrz TabView jest odtwarzane przy każdym przełączeniu. Od iOS 18 TabView może przechowywać View w pamięci — w tym przypadku .task nie uruchamia się ponownie. Użyj .task(id:) z identyfikatorem zakładki, jeśli potrzebujesz przeładowywać dane przy każdym przełączeniu.
SwiftUI anuluje zadanie przy zniknięciu View. Jeśli zapytanie URLSession było wewnątrz zadania, również zostanie anulowane. Jeśli zadanie nie obsługuje kooperatywnego anulowania (np. nie sprawdza isCancelled), będzie kontynuować wykonanie, ale jego wynik nie zostanie zastosowany do stanu, ponieważ View już nie istnieje.
Tak, można dodać kilka modyfikatorów .task do jednego View. Każdy tworzy niezależne zadanie. Jest to wygodne do rozdzielenia różnych źródeł danych: jeden .task do ładowania profilu, drugi do subskrypcji WebSocket, trzeci do monitorowania geolokalizacji.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również