SwiftTaskで失敗したタスクによって異なるエラー処理を行う方法
記事内に広告を含む場合があります。記事内で紹介する商品を購入することで、当サイトに売り上げの一部が還元されることがあります。
SwiftTaskで複数のタスクを.successでチェーンさせ、失敗したタスクによって異なるエラー処理を行う方法について解説したいと思います。
なお、この方法はSwiftTaskの作者であるYasuhiro Inami(@inamiy)さんに教えて頂きました。ありがとうございます!
タスクを作る
まずはいつも通りタスクを作りましょう。task1, task2, task3のエラー型がそれぞれBool, Double, Stringとなっており、統一されていない点に注目です。
func task1() -> Task<(),Int,Bool> { return Task<(),Int,Bool>() { (progress, fulfill, reject, configure) in fulfill(123) } } func task2() -> Task<(),Int,Double> { return Task<(),Int,Double>() { (progress, fulfill, reject, configure) in fulfill(123) } } func task3() -> Task<(),Int,String> { return Task<(),Int,String>() { (progress, fulfill, reject, configure) in reject("error3") } }
エラーを統一して扱うためのenumを作る
それぞれのタスクのエラーを統一して扱うためのenumを作ります。タスクの数だけcaseを作り、付属型をそれぞれのエラー型にします。
これにより、異なる型をenumで包んで統一的に扱えるようになります。
この時、実はWrappedError.ByTask1は、BoolをWrappedErrorに変換する関数として使用できます。
なお、case Cancelledは外部からのキャンセルを内部的なリジェクトに変換するためのものです。
enum WrappedError { case ByTask1(Bool) case ByTask2(Double) case ByTask3(String) /// For external cancellation -> internal rejection conversion. case Cancelled }
タスクのエラー型のみを変換する関数を作る
タスクのエラー型のみを変換する関数を作ります。つまりTask<Progress,Value,Error>型を、Task<Progress,Value,WrappedError>に変換するわけです。
この変換にはErrorからWrappedErrorへの変換も必要なので、その変換関数を引数として渡す形になっています。
extension Task { func _mapError(f: Error -> WrappedError) -> Task<Progress, Value, WrappedError> { return self.failure { error, isCancelled -> Task<Progress, Value, WrappedError> in if let error = error { return Task<Progress, Value, WrappedError>(error: f(error)) } else { // converts external cancellation -> internal rejection return Task<Progress, Value, WrappedError>(error: .Cancelled) } } } }
タスクのチェーンを作って実行する
最後の.onで、switchを使ってタスクごとに分岐させ、実際のエラー情報を取り出します。
task1()._mapError(WrappedError.ByTask1) .success { _ in task2()._mapError(WrappedError.ByTask2) } .success { _ in task3()._mapError(WrappedError.ByTask3) } .on(failure: { error, isCancelled in guard let error = error else { return } switch error { case let .ByTask1(error1): print(error1) case let .ByTask2(error2): print(error2) case let .ByTask3(error3): print(error3) default: () } })
タスクのエラー型だけを置き換えるヘルパー関数を作る
やりたいことは上記の方法で実現できたのですが、タスクをチェーンさせている箇所で毎回._mapError(WrappedError.ByTask1)などを呼び出すのが面倒です。
そこでタスクをチェーンさせる前に、タスクのエラー型のみ置き換えるヘルパー関数を作りました。
なお、SwiftTaskのタスクはインスタンス化された時点で実行開始されるので、変換前のタスクをクロージャとして受け取り、変換後のタスクもクロージャで包むことによって、実際に必要とされるタイミングまでインスタンス化されないようにしています。
この関数の作成にあたっては、熊谷 友宏(@es_kumagai)さんからアドバイスを頂きました。ありがとうございました!
func wrappedErrorTask<P,V,E>(task: () -> Task<P,V,E>, f: E -> WrappedError) -> () -> Task<P,V,WrappedError> { return { task()._mapError(f) } }
このヘルパー関数によって事前にタスクを変換しておけば、タスクをチェーンさせるコードをシンプルに書くことができます。
let wrapped1 = wrappedErrorTask(task1, f: WrappedError.ByTask1) let wrapped2 = wrappedErrorTask(task2, f: WrappedError.ByTask2) let wrapped3 = wrappedErrorTask(task3, f: WrappedError.ByTask3) wrapped1() .success { _ in wrapped2() } .success { _ in wrapped3() } .on(failure: { error, isCancelled in guard let error = error else { return } switch error { case let .ByTask1(error1): print(error1) case let .ByTask2(error2): print(error2) case let .ByTask3(error3): print(error3) default: () } })
サンプルはこちら
今回紹介したテクニックを使ったコードをGitHubに置いているので、よかったらチェックしてみてください ⇒ [Test] Add wrappedErrorTask · akio0911/SwiftTask@902335d
@akio0911はこう思った。
enumのcaseが関数として使えることや、インスタンス化のタイミングを遅らせるためにクロージャで包み込むテクニックはとても勉強になりました。
Yasuhiro Inami(@inamiy)さんさん、熊谷 友宏(@es_kumagai)さん、ありがとうございました!
関連記事
この記事が気に入ったら「いいね!」しよう
Twitterで更新情報をゲット!