使用 swift async/await 串接 closure 的方法

如果一個專案是從 Swift 還沒有 combine 的時代就開始進行,那專案中會有大量的 @escaping closure 在做非同步的資料傳遞。在 Swift 5.5 之後,Swift 加進了 async/await 的語法,讓需要進行非同步的任務,不再需要在撰寫的時候有一堆的大括號。

所以

現在會有一個新的情境,以前的 lib 已經寫好 URLRequest 的方法,並使用 closure 進行傳值。把已經寫好的 escaping closure 全部改寫意義可能不大,而且也伴隨著一定程度的風險。因此,能另外寫一個 func 進行 async/await 轉接,並保留原來的 closure,是一個風險較低的選項。

包一層 async func 的方法

原來發送 request 的類別


// 將 Alamofire 包起來的 Adapter,每個 func 只是使用 Alamofire 發送 URL Request
class AlamofireAdapter {
    
    // 原來的 closure func
    func getCoinMarketsInfo(coutPerPage: Int, page: Int, sparkLine: Bool = false, completion: @escaping (Result<[CoinMarketInfoElement], Error>) -> Void) {
        // 省略實作 implement code omitted
        
        // implemented code omitted
        completion(result)
    }
}

擴充 request 到 async/await

// 對 AlamofireAdapter 進行擴充
extension AlamofireAdapter {
    // 開出能使用 await 的 func,但實際上是呼叫原來的 getCoinMarketsInfo
    func getCoinList(countPerPage: Int, page: Int) async -> Result<[CoinMarketInfoElement], Error> {
        
        // 進行轉接
        return await withCheckedContinuation { continuation in
            
            alamofireAdapter.getCoinMarketsInfo(coutPerPage: countPerPage, page: page, sparkLine: true) { result in
                continuation.resume(returning: result)
            }
        }
    }
}

使用 withCheckedContinuation() 就可以將以前的 closure 資料,使用 continuation 進行 async return。

ContentView 使用 getCoinList()


func fechList() {
        
        Task {
            let result = await manager.getCoinList()
            switch result {
                case .success(let list):
                    
                    DispatchQueue.main.async { [weak self] in
                        self?.updateLastFetchTime()
                        self?.cryptoList = list
                    }
                    Logger.log("you got list, count: \(list.count), first: \(String(describing: list.first))")
                case .failure(let failure):
                    Logger.log("fetch market got error: \(failure), description: \(failure.localizedDescription)")
            }
        }
    }

使用在 SwiftUI 的 View 時

# 在 SwiftUI 上使用時

import SwiftUI

struct ContentView: View {
    
    private let alamofireAdapter: AlamofireAdapter = .init()
    
    @State var cryptoList: [CoinMarketInfoElement] = []
    
    var body: some View {
        
        Text("this is test")
            .task {
                fetchList()
            }
    }
    
    private func fetchList() {
        let result = await alamofireAdapter.getCoinList()
        switch result {
            case .success(let list):
                
                DispatchQueue.main.async { [self] in
                    self.cryptoList = list
                }
            case .failure(let failure):
                print("fetch market got error: \(failure), description: \(failure.localizedDescription)")
    }
}

結尾

請注意 async/await 還是需要專案最低支援在 iOS 13 以上,如果你的專案沒辦法低於 iOS 13,那還是沒辦法使用 async/await。

在我個人的 side project 上,我已經試著使用 SwiftUI 寫一些小專案,在使用 async/await 後,很多非同步的 feature 處理的程式碼,在人類眼中非常好讀。或許在 Apple 不斷的推進 Xcdoe 最低上架版本後,我會不斷的增加 async/await 程式碼的比重。目前,在專案上我仍然有很多 closure 在進行傳值。

相關資料

Swift evolution on continuation

WWDC21: - Meet async/await in Swift

swift.org - Concurrency

Hacking With Swift 的文章

Apple 開發者論壇的討論

在 Swift 論壇上對向前支援非常激烈的討論

Swift Concurrency 向下支援的文章

Updated: