iOS Concurrency 處理異步任務

在 iOS 15 (Swift 5.5) 之前,我們通常使用 Closure (Completion Handler) 來處理異步任務(如網路請求)。但隨著業務變複雜,這種寫法容易導致 "Callback Hell"。

Swift 引進了 Structured Concurrency (結構化並發),讓異步程式碼寫起來像同步程式碼一樣直觀、安全。

為什麼需要 Async/Await ?

過去的痛:Callback Hell

假設我們有一個需求:

  1. 先下載圖片 ID。
  2. 拿到 ID 後,下載圖片本身。
  3. 下載完後對圖片進行處理。

使用舊式的 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,專門代表主執行緒。

總結

  1. async: 標記函式為異步。
  2. await: 暫停當前執行,等待異步結果。
  3. Task: 從同步環境進入異步環境的橋樑。
  4. async let: 讓多個任務平行執行。
  5. @MainActor: 確保 UI 更新在主執行緒,取代 DispatchQueue.main