.task { } — nó là gì, bổ tố async và tải dữ liệu trong View

Tác giả: IT Sectr Đã đăng: 2026-06-26 Thời gian đọc: 9 phút

.task { } là một bổ tố trong SwiftUI được giới thiệu trong iOS 15, khởi chạy một thao tác bất đồng bộ khi View xuất hiện và tự động hủy nó khi View biến mất. Không giống như .onAppear, thực thi mã đồng bộ mà không có khả năng hủy, .task làm việc với ngữ cảnh async/await và tôn trọng vòng đời của View: khi View biến mất, SwiftUI gọi cancel() trên Task đã tạo. Điều này ngăn chặn rò rỉ bộ nhớ và việc thực thi các thao tác sau khi View không còn cần cập nhật nữa. Theo Apple WWDC Session 10132 — Meet async/await in SwiftUI (2024), .task là cách ưu tiên để tải dữ liệu trong SwiftUI vì nó hoạt động an toàn với Structured Concurrency và tự động quản lý thời gian sống của thao tác bất đồng bộ.

Các điểm chính

  • .task { } — bổ tố SwiftUI để tải dữ liệu bất đồng bộ khi View xuất hiện, khả dụng từ iOS 15.
  • Tự động hủy — khi View biến mất, SwiftUI hủy Task, ngăn chặn rò rỉ bộ nhớ.
  • Ngữ cảnh async/await — bên trong .task, các lệnh gọi async khả dụng mà không cần DispatchQueue hay Combine.
  • .task(id:) — biến thể với định danh khởi động lại tác vụ khi giá trị chỉ định thay đổi.
  • Structured Concurrency — .task hỗ trợ Structured Concurrency và TaskGroup cho các thao tác song song.

.task { } trong SwiftUI là gì

.task { } là một bổ tố View tạo ra một Task trong ngữ cảnh async khi View xuất hiện trên màn hình. SwiftUI thực thi closure được cung cấp trong một luồng nền, giữ cho luồng chính rảnh cho các thao tác UI. Khi View biến mất, SwiftUI tự động hủy Task thông qua cơ chế Structured Concurrency — điều này đảm bảo rằng thao tác bất đồng bộ không tiếp tục thực thi sau khi kết quả của nó không còn cần thiết nữa.

Theo Apple — Swift Programming Language (2025), .task sử dụng khái niệm Structured Concurrency được giới thiệu trong Swift 5.5. Mỗi .task tạo ra một tác vụ con trong tác vụ của View cha. Nếu tác vụ cha bị hủy (View biến mất), tất cả các tác vụ con cũng tự động bị hủy. Điều này đơn giản hóa triệt để việc quản lý vòng đời của các thao tác bất đồng bộ so với việc lưu trữ thủ công các tham chiếu đến DispatchWorkItem hoặc AnyCancellable.

Không giống như cách tiếp cận truyền thống với @State + gọi thủ công trong .onAppear, .task không yêu cầu lưu trữ tham chiếu đến Task để hủy sau này. SwiftUI tự động thực hiện việc này, giảm mã boilerplate và loại bỏ rủi ro quên hủy một tác vụ.

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: khác biệt chính

Nhiều nhà phát triển đã quen với việc tải dữ liệu trong .onAppear, nhưng với sự xuất hiện của async/await và .task, cách tiếp cận này đã trở nên lỗi thời. .onAppear thực thi mã một cách đồng bộ — đối với các thao tác bất đồng bộ bên trong .onAppear, cần phải bọc lệnh gọi trong Task { } và giữ thủ công tham chiếu để có thể hủy. .task thực hiện việc này tự động.

Đặc điểm.task { }.onAppear
Ngữ cảnh asyncAsync/await tích hợpCần wrapper Task { }
Tự động hủyCó, khi View biến mấtKhông, phải thực hiện thủ công
Structured ConcurrencyHỗ trợKhông hỗ trợ
Thực thi lạiChỉ khi id thay đổiMỗi lần View xuất hiện
Khuyến nghị AppleCách ưu tiênCho thao tác đồng bộ

.onAppear vẫn hữu ích cho các thao tác đồng bộ — ví dụ, ghi log hoặc thiết lập UI ban đầu. Nhưng để tải dữ liệu bất đồng bộ, yêu cầu mạng, thao tác cơ sở dữ liệu hoặc hệ thống tệp, hãy sử dụng .task. Nó an toàn hơn và sạch hơn từ góc độ kiến trúc.

swift
// ❌ Cách cũ: Task trong .onAppear không hủy
var loadTask: Task<Void, Never>?
func body() { var body: some View { Text("") }
    .onAppear {
        loadTask = Task { await loadData() }
    }
    .onDisappear { loadTask?.cancel() }

// ✅ Cách mới: .task quản lý việc hủy
func body() { var body: some View { Text("") }
    .task { await loadData() }

.task(id:) — khởi động lại khi dữ liệu thay đổi

Bổ tố .task(id:) chấp nhận một tham số bổ sung — một định danh. Khi giá trị của định danh thay đổi, SwiftUI hủy tác vụ hiện tại và bắt đầu một tác vụ mới với định danh mới. Điều này lý tưởng cho các màn hình nơi dữ liệu phụ thuộc vào một tham số đã chọn — ví dụ, danh sách bài viết theo danh mục hoặc chi tiết sản phẩm theo 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 {
            // xử lý lỗi
        }
    }
}

Khi categoryId thay đổi, SwiftUI hủy yêu cầu trước đó và bắt đầu yêu cầu mới. Điều này đặc biệt quan trọng khi chuyển đổi danh mục nhanh — các yêu cầu cũ sẽ không cạnh tranh với yêu cầu mới để cập nhật trạng thái. Nếu không có .task(id:), bạn sẽ phải theo dõi thủ công các thay đổi qua .onChange và quản lý Task thủ công.

Hủy tác vụ và kiểm tra isCancelled

Mặc dù .task tự động hủy tác vụ khi View biến mất, bản thân thao tác bất đồng bộ phải hợp tác kiểm tra việc hủy. Swift sử dụng mô hình hủy hợp tác — Task.cancel() không dừng thực thi một cách cưỡng chế, mà chỉ đặt cờ isCancelled. Mã bên trong tác vụ nên định kỳ kiểm tra cờ này.

swift
struct LoadingView: View {
    @State var progress: Double = 0
    
    var body: some View {
        ProgressView(value: progress)
            .task {
                for i in 0..<100 {
                    // Kiểm tra hủy
                    try Task.checkCancellation()
                    
                    await Task.sleep(nanoseconds: 50_000_000)
                    progress = Double(i + 1) / 100.0
                }
            }
    }
}

Task.checkCancellation() ném CancellationError nếu tác vụ đã bị hủy. Đây là cách kiểm tra đơn giản nhất — nó hoạt động trong bất kỳ ngữ cảnh async nào. Một cách thay thế là kiểm tra Task.isCancelled thủ công trước các thao tác tốn kém. Đối với URLSession, các yêu cầu mạng tự động bị hủy khi tác vụ bị hủy, vì URLSession hỗ trợ Structured Concurrency một cách tự nhiên.

Ví dụ thực tế về .task

.task phù hợp với nhiều kịch bản: từ tải JSON đơn giản đến các thao tác song song phức tạp với TaskGroup. Hãy xem xét ba trường hợp sử dụng điển hình.

Tải với xử lý lỗi

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("Tải thất bại")
            }
        }
        .task {
            defer { isLoading = false }
            do {
                profile = await APIClient().fetchProfile()
            } catch {
                // hồ sơ vẫn là nil
            }
        }
    }
}

Tải song song với TaskGroup

swift
struct DashboardView: View {
    @State var stats: DashboardStats?
    
    var body: some View {
        Text("Bảng điều khiển")
            .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
            }
    }
}

Lỗi thường gặp với .task

Lỗi phổ biến nhất là thay đổi các thuộc tính UI bên trong .task mà không chuyển sang luồng chính. Mặc dù SwiftUI tự động trả các cập nhật về luồng chính khi sửa đổi @State trong ngữ cảnh async, việc thao tác trực tiếp các phần tử UIKit bên trong .task có thể gây ra sự cố.

  • Quên try/catch — .task không tự động xử lý lỗi. Tất cả các hàm ném lỗi phải được bọc trong do/catch, nếu không ứng dụng sẽ bị sập.
  • Điều kiện tranh đua — nếu nhiều .task(id:) được khởi chạy với các id khác nhau và cập nhật cùng một trạng thái, có thể xảy ra điều kiện tranh đua. Sử dụng các thuộc tính riêng biệt cho các nguồn dữ liệu khác nhau.
  • Thao tác đồng bộ dài — .task không làm cho mã đồng bộ trở thành bất đồng bộ. Nếu có công việc đồng bộ nặng bên trong .task, hãy bọc nó trong Task.detached hoặc chuyển nó sang một phương thức async riêng.
  • Bỏ qua CancellationError — khi kiểm tra Task.checkCancellation(), CancellationError nên được truyền lên trên, không bị triệt tiêu. Triệt tiêu việc hủy có thể dẫn đến rò rỉ bộ nhớ.
swift
// ❌ Lỗi: không xử lý lỗi
.task {
    let data = await fetchData() // sập khi có lỗi!
    items = data
}

// ✅ Đúng: do/catch
.task {
    do {
        items = await fetchData()
    } catch {
        errorMessage = error.localizedDescription
    }
}

Câu hỏi thường gặp

Sự khác biệt giữa .task và sử dụng Task { } trong .onAppear là gì?

.task tự động hủy tác vụ khi View biến mất và hỗ trợ Structured Concurrency. Task { } trong .onAppear yêu cầu giữ thủ công tham chiếu đến tác vụ và gọi cancel() trong .onDisappear. .task cũng dễ đọc hơn — nó chỉ rõ rằng việc tải dữ liệu là một phần của vòng đời View.

Có thể sử dụng .task để đăng ký một publisher không?

Có, để đăng ký AsyncSequence hoặc AsyncStream, hãy sử dụng for await value in publisher.values bên trong .task. Điều này hoạt động với cả async/await và Combine thông qua phần mở rộng Publisher.values. Khi View biến mất, vòng lặp sẽ tự động kết thúc và đăng ký sẽ bị hủy.

.task hoạt động với TabView như thế nào — tác vụ có khởi động lại khi chuyển tab không?

Có, nếu View bên trong TabView được tạo lại mỗi khi chuyển. Từ iOS 18, TabView có thể giữ Views trong bộ nhớ — trong trường hợp này .task không khởi động lại. Sử dụng .task(id:) với định danh tab nếu bạn cần tải lại dữ liệu mỗi khi chuyển tab.

Điều gì xảy ra nếu View với .task biến mất trước khi yêu cầu hoàn tất?

SwiftUI sẽ hủy tác vụ khi View biến mất. Nếu yêu cầu URLSession nằm trong tác vụ, nó cũng sẽ bị hủy. Nếu tác vụ không hỗ trợ hủy hợp tác (ví dụ, không kiểm tra isCancelled), nó sẽ tiếp tục chạy, nhưng kết quả của nó sẽ không được áp dụng cho trạng thái vì View không còn tồn tại.

Có thể sử dụng nhiều bổ tố .task trên một View không?

Có, bạn có thể thêm nhiều bổ tố .task trên một View. Mỗi bổ tố tạo một tác vụ độc lập. Điều này hữu ích để tách biệt các nguồn dữ liệu khác nhau: một .task để tải hồ sơ, một .task khác để đăng ký WebSocket, một .task thứ ba để theo dõi vị trí địa lý.

Tổng kết

  • .task { } — bổ tố SwiftUI cho các thao tác bất đồng bộ với tự động hủy khi View biến mất.
  • Hỗ trợ async/await — bên trong .task, ngữ cảnh async đầy đủ có sẵn mà không cần wrapper Task.
  • .task(id:) — khởi động lại tác vụ khi định danh thay đổi, thay thế .onChange thủ công.
  • Hủy hợp tác — sử dụng Task.checkCancellation() để kiểm tra hủy bên trong tác vụ.
  • Structured Concurrency — .task hỗ trợ TaskGroup và các thao tác song song với hủy tác vụ con.
  • Thay thế .onAppear — để tải dữ liệu bất đồng bộ, sử dụng .task thay vì mẫu .onAppear + Task + .onDisappear.
  • Xử lý lỗi — tất cả các lệnh gọi async bên trong .task phải được bọc trong do/catch để ngăn sập.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm