.task { } คือตัวปรับแก้ใน SwiftUI ที่เปิดตัวใน iOS 15 ซึ่งเริ่มดำเนินการแบบอะซิงโครนัสเมื่อ View ปรากฏขึ้นและยกเลิกโดยอัตโนมัติเมื่อ 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 { } คือตัวปรับแก้ View ที่สร้าง Task ในบริบท async เมื่อ View ปรากฏขึ้นบนหน้าจอ SwiftUI จะดำเนินการ closure ที่ให้ไว้ในเธรดพื้นหลัง โดยปล่อยให้เธรดหลักว่างสำหรับการดำเนินการ 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 ทำสิ่งนี้โดยอัตโนมัติ ลดโค้ดเทมเพลตและขจัดความเสี่ยงในการลืมยกเลิกงาน
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
}
}
}
}
นักพัฒนาหลายคนคุ้นเคยกับการโหลดข้อมูลใน .onAppear แต่ด้วยการมาของ async/await และ .task วิธีการนี้จึงล้าสมัย .onAppear ดำเนินการโค้ดแบบซิงโครนัส — สำหรับการดำเนินการแบบอะซิงโครนัสภายใน .onAppear จำเป็นต้องห่อการเรียกใน Task { } และเก็บข้อมูลอ้างอิงด้วยตนเองสำหรับการยกเลิกที่เป็นไปได้ .task ทำสิ่งนี้โดยอัตโนมัติ
| คุณลักษณะ | .task { } | .onAppear |
|---|---|---|
| บริบท async | async/await ในตัว | ต้องการ wrapper Task { } |
| การยกเลิกอัตโนมัติ | ใช่ เมื่อ View หายไป | ไม่ ต้องดำเนินการด้วยตนเอง |
| Structured Concurrency | รองรับ | ไม่รองรับ |
| การทำงานซ้ำ | เฉพาะเมื่อ id เปลี่ยนแปลง | ทุกครั้งที่ View ปรากฏ |
| คำแนะนำของ Apple | วิธีที่ต้องการ | สำหรับการดำเนินการแบบซิงโครนัส |
.onAppear ยังคงมีประโยชน์สำหรับการดำเนินการแบบซิงโครนัส — ตัวอย่างเช่น การบันทึกหรือการตั้งค่า UI เริ่มต้น แต่สำหรับการโหลดข้อมูลแบบอะซิงโครนัส คำขอเครือข่าย การทำงานกับฐานข้อมูลหรือระบบไฟล์ ให้ใช้ .task ซึ่งปลอดภัยกว่าและสะอาดกว่าจากมุมมองทางสถาปัตยกรรม
// ❌ วิธีเดิม: 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:) ยอมรับพารามิเตอร์เพิ่มเติม — ตัวระบุ เมื่อค่าของตัวระบุเปลี่ยนแปลง SwiftUI จะยกเลิกงานปัจจุบันและเริ่มงานใหม่ด้วยตัวระบุใหม่ ซึ่งเหมาะสำหรับหน้าจอที่ข้อมูลขึ้นอยู่กับพารามิเตอร์ที่เลือก — ตัวอย่างเช่น รายการบทความตามหมวดหมู่หรือรายละเอียดสินค้าตาม 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 {
// จัดการข้อผิดพลาด
}
}
}
เมื่อ categoryId เปลี่ยนแปลง SwiftUI จะยกเลิกคำขอก่อนหน้าและเริ่มคำขอใหม่ ซึ่งสำคัญอย่างยิ่งเมื่อสลับหมวดหมู่อย่างรวดเร็ว — คำขอเก่าจะไม่แข่งขันกับคำขอใหม่ในการอัปเดตสถานะ หากไม่มี .task(id:) คุณจะต้องติดตามการเปลี่ยนแปลงด้วยตนเองผ่าน .onChange และจัดการ Task ด้วยตนเอง
แม้ว่า .task จะยกเลิกงานโดยอัตโนมัติเมื่อ View หายไป แต่การดำเนินการแบบอะซิงโครนัสจะต้องตรวจสอบการยกเลิกแบบร่วมมือกัน Swift ใช้โมเดลการยกเลิกแบบร่วมมือ — Task.cancel() ไม่ได้หยุดการทำงานโดยบังคับ แต่เพียงตั้งค่าแฟล็ก isCancelled เท่านั้น โค้ดภายในงานควรตรวจสอบแฟล็กนี้เป็นระยะ
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 เหมาะสำหรับหลายสถานการณ์: ตั้งแต่การโหลด JSON อย่างง่ายไปจนถึงการดำเนินการแบบขนานที่ซับซ้อนด้วย TaskGroup มาดูสามกรณีการใช้งานทั่วไป
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
}
}
}
}
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
}
}
}
ข้อผิดพลาดที่พบบ่อยที่สุดคือการเปลี่ยนแปลงคุณสมบัติ UI ภายใน .task โดยไม่สลับไปยังเธรดหลัก แม้ว่า SwiftUI จะส่งคืนการอัปเดตไปยังเธรดหลักโดยอัตโนมัติเมื่อแก้ไข @State ในบริบท async แต่การจัดการองค์ประกอบ UIKit โดยตรงภายใน .task อาจทำให้เกิดการหยุดทำงาน
// ❌ ข้อผิดพลาด: ไม่มีการจัดการข้อผิดพลาด
.task {
let data = await fetchData() // หยุดทำงานเมื่อมีข้อผิดพลาด!
items = data
}
// ✅ ถูกต้อง: do/catch
.task {
do {
items = await fetchData()
} catch {
errorMessage = error.localizedDescription
}
}
คำถามที่พบบ่อย
.task ยกเลิกงานโดยอัตโนมัติเมื่อ View หายไปและรองรับ Structured Concurrency Task { } ใน .onAppear ต้องเก็บข้อมูลอ้างอิงไปยังงานด้วยตนเองและเรียก cancel() ใน .onDisappear .task ยังอ่านง่ายกว่า — มันบ่งชี้อย่างชัดเจนว่าการโหลดข้อมูลเป็นส่วนหนึ่งของวงจรชีวิตของ View
ใช่ หากต้องการสมัครรับ AsyncSequence หรือ AsyncStream ให้ใช้ for await value in publisher.values ภายใน .task ซึ่งทำงานได้ทั้งกับ async/await และ Combine ผ่านส่วนขยาย Publisher.values เมื่อ View หายไป การวนซ้ำจะสิ้นสุดโดยอัตโนมัติและการสมัครรับจะถูกยกเลิก
ใช่ หาก View ภายใน TabView ถูกสร้างใหม่ทุกครั้งที่เปลี่ยนแท็บ ตั้งแต่ iOS 18 เป็นต้นไป TabView สามารถเก็บ Views ไว้ในหน่วยความจำ — ในกรณีนี้ .task จะไม่เริ่มใหม่ ใช้ .task(id:) พร้อมตัวระบุแท็บหากคุณต้องการโหลดข้อมูลใหม่ทุกครั้งที่เปลี่ยนแท็บ
SwiftUI จะยกเลิกงานเมื่อ View หายไป หากคำขอ URLSession อยู่ภายในงาน มันก็จะถูกยกเลิกเช่นกัน หากงานไม่รองรับการยกเลิกแบบร่วมมือ (เช่น ไม่ตรวจสอบ isCancelled) มันจะทำงานต่อไป แต่ผลลัพธ์จะไม่ถูกนำไปใช้กับสถานะเนื่องจาก View ไม่มีอยู่อีกต่อไป
ใช่ คุณสามารถเพิ่มตัวปรับแก้ .task หลายตัวบน View เดียวได้ แต่ละตัวสร้างงานอิสระ ซึ่งมีประโยชน์สำหรับการแยกแหล่งข้อมูลที่แตกต่างกัน: .task หนึ่งสำหรับโหลดโปรไฟล์ อีกอันสำหรับสมัครรับ WebSocket อันที่สามสำหรับติดตามตำแหน่งทางภูมิศาสตร์
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม