.task { } — co to jest, modyfikator async i ładowanie danych w View

Autor: IT Sectr Opublikowano: 2026-06-26 Czas czytania: 9 min

.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 { } — modyfikator SwiftUI do asynchronicznego ładowania danych przy pojawieniu się View, dostępny od iOS 15.
  • Automatyczne anulowanie — przy zniknięciu View SwiftUI anuluje Task, zapobiegając wyciekom pamięci.
  • Kontekst async/await — wewnątrz .task dostępne są wywołania async bez konieczności używania DispatchQueue lub Combine.
  • .task(id:) — wariant z identyfikatorem restartuje zadanie przy zmianie wskazanej wartości.
  • Structured Concurrency — .task obsługuje Structured Concurrency i TaskGroup dla operacji równoległych.

Co to jest .task { } w SwiftUI

.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.

swift
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
            }
        }
    }
}

.task vs .onAppear: kluczowe różnice

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 asyncWbudowany async/awaitWymaga opakowania Task { }
AutoanulowanieTak, przy zniknięciu ViewNie, trzeba zaimplementować ręcznie
Structured ConcurrencyObsługujeNie obsługuje
Ponowne uruchomienieTylko przy zmianie idZa każdym razem przy pojawieniu się
Zalecenie ApplePreferowany sposóbDla 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.

swift
// ❌ 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() }

.task(id:) — restart przy zmianie danych

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.

swift
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.

Anulowanie zadań i sprawdzanie isCancelled

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ę.

swift
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.

Praktyczne przykłady .task

.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.

Ładowanie z obsługą błędów

swift
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
            }
        }
    }
}

Równoległe ładowanie z TaskGroup

swift
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
            }
    }
}

Typowe błędy z .task

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.

  • Zapomniałeś try/catch — .task nie obsługuje błędów automatycznie. Wszystkie funkcje rzucające wewnątrz muszą być opakowane w do/catch, w przeciwnym razie aplikacja ulegnie awarii.
  • Race condition — jeśli kilka .task(id:) jest uruchomionych z różnymi id i aktualizuje ten sam stan, możliwe są wyścigi. Używaj oddzielnych właściwości dla różnych źródeł danych.
  • Długie operacje synchroniczne — .task nie czyni kodu synchronicznego asynchronicznym. Jeśli wewnątrz .task jest ciężka praca synchroniczna, opakuj ją w Task.detached lub przenieś do osobnej metody async.
  • Ignorowanie CancellationError — przy sprawdzaniu Task.checkCancellation() błąd CancellationError powinien być propagowany w górę, a nie tłumiony. Tłumienie anulowania może prowadzić do wycieków pamięci.
swift
// ❌ 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

Jaka jest różnica między .task a użyciem Task { } w .onAppear?

.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.

Czy można użyć .task do subskrypcji publisherów?

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.

Jak .task działa z TabView — czy zadanie jest restartowane przy przełączaniu zakładek?

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.

Co się stanie, jeśli View z .task zniknie przed zakończeniem zapytania?

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.

Czy można użyć wielu .task na jednym View?

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

  • .task { } — modyfikator SwiftUI do operacji asynchronicznych z autoanulowaniem przy zniknięciu View.
  • Obsługa async/await — wewnątrz .task dostępny jest pełny kontekst async bez konieczności opakowania Task.
  • .task(id:) — restartuje zadanie przy zmianie identyfikatora, zastępuje ręczny .onChange.
  • Kooperatywne anulowanie — używaj Task.checkCancellation() do sprawdzania anulowania wewnątrz zadania.
  • Structured Concurrency — .task obsługuje TaskGroup i operacje równoległe z anulowaniem zadań podrzędnych.
  • Zastąpienie .onAppear — do asynchronicznego ładowania danych używaj .task zamiast kombinacji .onAppear + Task + .onDisappear.
  • Obsługa błędów — wszystkie wywołania async wewnątrz .task muszą być opakowane w do/catch, aby zapobiec awarii.

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.

Omów projekt

Przeczytaj również