游諭 Swift Dev 🦄

使用 Async 輕量非同步套件鏈式調用非同步行為

Async: github.com/duemunk/Async

2019 年介紹 Combine2019鐵人賽介紹 Combine 系列, 對於連續的非同步事件的調用, 我們會遇到回呼地獄(Callback hell), 也就是多筆具相依的非同步步驟響應辦法.

## Async 的解法 相較於原本的 DispatchQueue 的版本:

DispatchQueue.global(qos: .userInitiated).async {
    let value = 10
    DispatchQueue.global(qos: .background).async {
        let text = "Score: \(value)"
        DispatchQueue.main.async {
            label.text = text
        }
    }
}
Async
.userInitiated { 10 }
.background { "Score: \($0)"}
.main { label.text = $0 }

Async很像瀑布一樣, 由上至下的思考非同步的執行流程.

Async 沒有對錯誤拋出處理 throws

在研究了一段時間發現, Async 很可惜的沒有對 Error handle 做處理, 決定研究了一下, 修改部分的程式碼, fork repo.

private class Reference<T> {
        var value: T?
/*新增*/    var error:Error? 
/*新增*/    var queue:GCD?   
}
public struct AsyncBlock<In, Out> {

    ...


    private static func async<O>(after seconds: Double? = nil,
                                 block: @escaping () throws -> O,
                                 queue: GCD) -> AsyncBlock<Void, O> {
        let reference = Reference<O>()
/*新增*/    reference.queue = queue
        let block = DispatchWorkItem(block: {
/*新增*/        do {
/*新增*/            reference.value = try block()
/*新增*/        }catch {
/*新增*/            reference.error = error
/*新增*/        }
        })
      ...
    }
 
    private func chain<O>(after seconds: Double? = nil,
                          block chainingBlock: @escaping (Out) throws -> O,
                          queue: GCD) -> AsyncBlock<Out, O> {
        let reference = Reference<O>()
/*新增*/            reference.queue = queue
        let dispatchWorkItem = DispatchWorkItem(block: {
/*新增*/        guard let value = self.output_.value else {
/*新增*/            return reference.error = self.output_.error!
/*新增*/        }
/*新增*/        do {
/*新增*/            reference.value = try chainingBlock(value)
/*新增*/        } catch {
/*新增*/            reference.error = error
/*新增*/        }
        })

    ...
    }

...

}

如此一來, 就可以對 AsyncBlock 拓展 catch(_:)

/*新增*/
@discardableResult
public func `catch`(respondBlock: @escaping (Error) -> Void) -> AsyncBlock<In,Out> {
    let queue = output_.queue!.queue
    let c = {
        if let error = self.output_.error {
            respondBlock(error)
        }
    }
    let item = DispatchWorkItem(block: c)
    block.notify(queue: queue, execute: item)
    return self
}

實作單元測試:

func testAsyncMainWithCatch() {
    let expectation = self.expectation(description: "Expected on main queue")
    var calledStuffAfterSinceAsync = false
    Async.main {
        try self.alwaysError()
    }.catch { (error) in
        #if targetEnvironment(simulator)
        XCTAssert(Thread.isMainThread, "Should be on main thread (simulator)")
        #else
        XCTAssertEqual(qos_class_self(), qos_class_main())
        #endif
        XCTAssert(calledStuffAfterSinceAsync, "Should be async")
        expectation.fulfill()
    }
    calledStuffAfterSinceAsync = true
    waitForExpectations(timeout: timeMargin, handler: nil)
}

以上, 於 fork 的 GitHub project 內持續補上 test case! GitHub 連結: https://github.com/ytyubox/Async

Tagged with: