.task { } — що це, модифікатор async і завантаження даних у View

Автор: IT Sectr Опубліковано: 2026-06-26 Час читання: 9 хв

.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 { } — модифікатор SwiftUI для асинхронного завантаження даних при появі View, доступний з iOS 15.
  • Автоматичне скасування — при зникненні View SwiftUI скасовує Task, запобігаючи витокам пам'яті.
  • Контекст async/await — в .task доступні async-виклики без необхідності в DispatchQueue або Combine.
  • .task(id:) — варіант з ідентифікатором перезапускає завдання при зміні вказаного значення.
  • Structured Concurrency — .task підтримує Structured Concurrency і TaskGroup для паралельних операцій.

Що таке .task { } у SwiftUI

.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 робить це автоматично, що зменшує кількість шаблонного коду та виключає ризик забути скасувати завдання.

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: ключові відмінності

Багато розробників звикли завантажувати дані в .onAppear, але з появою async/await і .task цей підхід застарів. .onAppear виконує код синхронно — для асинхронних операцій всередині .onAppear потрібно обгортати виклик у Task { } і вручну зберігати посилання на нього для можливого скасування. .task робить це автоматично.

Характеристика.task { }.onAppear
Async контекстВбудований async/awaitПотребує Task { } обгортки
АвтоскасуванняТак, при зникненні ViewНі, потрібно реалізувати вручну
Structured ConcurrencyПідтримуєНе підтримує
Повторний запускТільки при зміні idЩоразу при появі
Рекомендація AppleКращий спосібДля синхронних операцій

.onAppear все ще корисний для синхронних операцій — наприклад, логування або початкового налаштування UI. Але для асинхронного завантаження даних, мережевих запитів, роботи з базою даних або файловою системою використовуй .task. Це безпечніше та чистіше з точки зору архітектури.

swift
// ❌ Застарілий підхід: 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:) — перезапуск при зміні даних

Модифікатор .task(id:) приймає додатковий параметр — ідентифікатор. Коли значення ідентифікатора змінюється, SwiftUI скасовує поточне завдання та запускає нове з новим ідентифікатором. Це ідеально підходить для екранів, де дані залежать від вибраного параметра — наприклад, список статей за категорією або деталі товару за 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 {
            // обробити помилку
        }
    }
}

Коли categoryId змінюється, SwiftUI скасовує попередній запит і запускає новий. Це особливо важливо при швидких перемиканнях категорій — старі запити не будуть конкурувати з новими за оновлення стану. Без .task(id:) довелося б вручну відстежувати зміни через .onChange і керувати Task вручну.

Скасування завдань і перевірка isCancelled

Хоча .task автоматично скасовує завдання при зникненні View, сама асинхронна операція повинна кооперативно перевіряти скасування. Swift використовує кооперативну модель скасування — Task.cancel() не зупиняє виконання примусово, а лише встановлює прапорець isCancelled. Код всередині завдання повинен періодично перевіряти цей прапорець.

swift
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

.task підходить для багатьох сценаріїв: від простого завантаження JSON до складних паралельних операцій з TaskGroup. Розглянемо три типових приклади використання.

Завантаження з обробкою помилок

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("Не вдалося завантажити")
            }
        }
        .task {
            defer { isLoading = false }
            do {
                profile = await APIClient().fetchProfile()
            } catch {
                // профіль залишається nil
            }
        }
    }
}

Паралельне завантаження з TaskGroup

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

Типові помилки з .task

Найбільш поширена помилка — мутація UI-властивостей всередині .task без перемикання на головний потік. Хоча SwiftUI автоматично повертає оновлення на головний потік при зміні @State в async-контексті, прямі маніпуляції з UIKit-елементами всередині .task можуть викликати crash.

  • Забув try/catch — .task не обробляє помилки автоматично. Всі функції, що викидають помилки, повинні бути обгорнуті в do/catch, інакше застосунок впаде.
  • Гонка даних — якщо кілька .task(id:) запущені з різними id і оновлюють один і той же стан, можливі гонки. Використовуй окремі властивості для різних джерел даних.
  • Довгі синхронні операції — .task не робить синхронний код асинхронним. Якщо всередині .task є важка синхронна робота, обгорни її в Task.detached або перенеси в окремий async-метод.
  • Ігнорування CancellationError — при перевірці Task.checkCancellation() помилка CancellationError повинна пробрасуватися нагору, а не пригнічуватися. Пригнічення скасування може призвести до витоку пам'яті.
swift
// ❌ Помилка: без обробки помилок
.task {
    let data = await fetchData() // падає при помилці!
    items = data
}

// ✅ Правильно: do/catch
.task {
    do {
        items = await fetchData()
    } catch {
        errorMessage = error.localizedDescription
    }
}

Часті запитання

У чому різниця між .task і використанням Task { } у .onAppear?

.task автоматично скасовує завдання при зникненні View і підтримує Structured Concurrency. Task { } у .onAppear вимагає ручного зберігання посилання на завдання та виклику cancel() у .onDisappear. .task також простіше читається — він явно вказує, що завантаження даних є частиною життєвого циклу View.

Чи можна використовувати .task для підписки на publisher?

Так, для підписки на AsyncSequence або AsyncStream використовуй for await value in publisher.values всередині .task. Це працює як з async/await, так і з Combine через розширення Publisher.values. При зникненні View ітерація автоматично завершиться, і підписка буде скасована.

Як .task працює з TabView — чи перезапускається завдання при перемиканні вкладок?

Так, якщо View всередині TabView перестворюється при кожному перемиканні. Починаючи з iOS 18 TabView може зберігати View у пам'яті — у цьому випадку .task не запускається повторно. Використовуй .task(id:) з ідентифікатором вкладки, якщо потрібно перезавантажувати дані при кожному перемиканні.

Що станеться, якщо View з .task зникне до завершення запиту?

SwiftUI скасує завдання при зникненні View. Якщо URLSession запит був всередині завдання, він також буде скасований. Якщо завдання не підтримує кооперативне скасування (наприклад, не перевіряє isCancelled), воно продовжить виконуватися, але його результат не буде застосовано до стану, оскільки View уже не існує.

Чи можна використовувати кілька .task на одному View?

Так, можна додати кілька .task модифікаторів на одне View. Кожен створює незалежне завдання. Це зручно для розділення різних джерел даних: один .task для завантаження профілю, другий для підписки на WebSocket, третій для моніторингу геолокації.

Підсумки

  • .task { } — модифікатор SwiftUI для асинхронних операцій з автоскасуванням при зникненні View.
  • Підтримка async/await — всередині .task доступний повноцінний async контекст без необхідності Task обгортки.
  • .task(id:) — перезапускає завдання при зміні ідентифікатора, замінює ручний .onChange.
  • Кооперативне скасування — використовуй Task.checkCancellation() для перевірки скасування всередині завдання.
  • Structured Concurrency — .task підтримує TaskGroup і паралельні операції зі скасуванням дочірніх завдань.
  • Заміна .onAppear — для асинхронного завантаження даних використовуй .task замість зв'язки .onAppear + Task + .onDisappear.
  • Обробка помилок — всі async виклики всередині .task повинні бути обгорнуті в do/catch для запобігання crash.

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також