iOS Concurrency 處理異步任務
在 iOS 15 (Swift 5.5) 之前,我們通常使用 Closure (Completion Handler) 來處理異步任務(如網路請求)。但隨著業務變複雜,這種寫法容易導致 "Callback Hell"。
Swift 引進了 Structured Concurrency (結構化並發),讓異步程式碼寫起來像同步程式碼一樣直觀、安全。
為什麼需要 Async/Await ?
過去的痛:Callback Hell
假設我們有一個需求:
- 先下載圖片 ID。
- 拿到 ID 後,下載圖片本身。
- 下載完後對圖片進行處理。
使用舊式的 Completion Handler,程式碼會長這樣:
func processImage(completion: @escaping (Image?, Error?) -> Void) {
downloadID { id, error in
guard let id = id else {
completion(nil, error)
return
}
downloadImage(id) { image, error in
guard let image = image else {
completion(nil, error)
return
}
process(image) { processedImage in
completion(processedImage, nil)
}
}
}
}
問題:
- 層層巢狀:閱讀困難。
- 錯誤處理分散:要在每個
guard裡手動呼叫completion(nil, error),只要忘記一個,App 就會卡住 (Hang)。
現在的解法:Async/Await
使用 async/await,同樣的邏輯變得像直線一樣:
func processImage() async throws -> Image {
let id = try await downloadID()
let image = try await downloadImage(id)
let processedImage = await process(image)
return processedImage
}
優點:
- 線性邏輯:從上往下讀,清晰易懂。
- 統一錯誤處理:可以使用標準的
do-catch。
語法詳解
定義異步函式 (async)
在函式參數後標記 async。如果會拋出錯誤,則加上 throws。
func fetchData() async throws -> String {
// 模擬網路延遲
// Task.sleep 在 Swift Concurrency 中是用來暫停任務的標準做法
try await Task.sleep(nanoseconds: 1 * 1_000_000_000)
return "Hello, Concurrency!"
}
呼叫異步函式 (await)
每當你呼叫一個 async 函式時,必須在前面加上 await。這代表**「程式執行到這裡會暫停 (Suspend),直到對方做完並回傳結果,我才繼續往下跑」**。
func updateUI() async {
do {
print("開始下載...")
let text = try await fetchData() // 這裡會暫停 1 秒
print("結果:\(text)")
} catch {
print("發生錯誤: \(error)")
}
}
進入異步環境 (Task)
你不能在一個同步的環境(例如 Button 的 action 或一般函式)直接呼叫 async 方法。你需要一個「橋樑」來進入異步世界,這個橋樑就是 Task。
Unstructured Task
Button("開始下載") {
// 建立一個新的異步任務環境
Task {
let data = try? await fetchData()
print(data)
}
}
SwiftUI .task
在 SwiftUI 中,最佳實踐是使用 .task 修飾符。它相當於 onAppear 的異步版本,而且當 View 消失時,它會自動取消正在執行的任務,節省資源。
Text("Loading...")
.task {
// 當 View 出現時執行,消失時自動取消
await loadData()
}
平行執行 (Async Let)
await 的特性是「等待」。如果你連續寫兩行 await,它們會依序執行 (Sequential)。
// 總共需耗時 2 秒 (1 + 1)
let user = await fetchUser() // 等 1 秒
let posts = await fetchPosts() // 再等 1 秒
如果你希望這兩個請求同時發出 (Parallel),可以使用 async let:
// 兩個任務同時開始執行
async let userRequest = fetchUser()
async let postsRequest = fetchPosts()
// 直到這行才真正等待兩個結果都回來 (總耗時約 1 秒)
let (user, posts) = await (userRequest, postsRequest)
MainActor (執行緒安全)
在 iOS 開發中,所有 UI 更新都必須在主執行緒 (Main Thread) 進行。
在舊時代,我們常寫 DispatchQueue.main.async { ... }。
在 Concurrency 時代,我們使用 @MainActor 屬性。
被標記為 @MainActor 的 Class 或 Function,編譯器會保證它們的程式碼一定是在 Main Thread 執行。
@MainActor // 標記整個 Class
class ViewModel: ObservableObject {
@Published var data = ""
func reload() async {
// 1. fetch 在背景執行 (非 Main Thread)
let result = await fetch()
// 2. 賦值時,因為這是 @MainActor 的類別
// 系統會自動切換回 Main Thread 來更新 data
self.data = result
}
}
什麼是 Actor?
actor 是 Swift 5.5 新增的一種型別 (類似 class),但它是執行緒安全 (Thread-safe) 的。它保證同一時間只有一個任務能存取它的狀態,完美解決了 Race Condition 問題。MainActor 就是系統預定義好的一個特殊 Actor,專門代表主執行緒。
總結
- async: 標記函式為異步。
- await: 暫停當前執行,等待異步結果。
- Task: 從同步環境進入異步環境的橋樑。
- async let: 讓多個任務平行執行。
- @MainActor: 確保 UI 更新在主執行緒,取代
DispatchQueue.main。