.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 делает это автоматически, что уменьшает количество boilerplate кода и исключает риск забыть отменить задачу.

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
// ❌ Legacy approach: Task in .onAppear without cancellation
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
    .onAppear {
        loadTask = Task { await loadData() }
    }
    .onDisappear { loadTask?.cancel() }

// ✅ Modern approach: .task manages cancellation
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 {
            // handle error
        }
    }
}

Когда categoryId изменяется, SwiftUI отменяет предыдущий запрос и запускает новый. Это особенно важно при быстрых переключениях категорий — старые запросы не будут конкурировать с новыми за обновление state. Без .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 {
                    // Check cancellation
                    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("Failed to load")
            }
        }
        .task {
            defer { isLoading = false }
            do {
                profile = await APIClient().fetchProfile()
            } catch {
                // profile stays nil
            }
        }
    }
}

Параллельная загрузка с TaskGroup

swift
struct DashboardView: View {
    @State var stats: DashboardStats?
    
    var body: some View {
        Text("Dashboard")
            .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, иначе приложение упадёт.
  • Гонка данных (race condition) — если несколько .task(id:) запущены с разными id и обновляют один и тот же state, возможны гонки. Используй отдельные свойства для разных источников данных.
  • Долгие синхронные операции — .task не делает синхронный код асинхронным. Если внутри .task есть тяжёлая синхронная работа, оберни её в Task.detached или перенеси в отдельный async-метод.
  • Игнорирование CancellationError — при проверке Task.checkCancellation() ошибка CancellationError должна пробрасываться наверх, а не подавляться. Подавление отмены может привести к утечке памяти.
swift
// ❌ Error: no error handling
.task {
    let data = await fetchData() // crashes on error!
    items = data
}

// ✅ Correct: 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), она продолжит выполняться, но её результат не будет применён к state, так как 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 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

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