Promiseパターンでできること
Promiseパターンを使うと何ができるのかを調べてみました。
- 非同期で処理を行い、処理終了後に特定のメソッドを呼び出す
- 失敗時の処理を登録しておくことで、最初の処理が失敗した時にそのメソッドが呼ばれる
- 複数の非同期処理を順番に実行する
- 複数の非同期処理を並行に実行して、全て成功したタイミングでコールバックメソッドを呼ぶ
プロミスパターンでは上の4つが特徴として上げられていました。
1と2は通信ライブラリやアニメーションのAPIに標準で付いている機能ですね。
3番はとても便利そうです。
複数の非同期処理を順番に実行する場合、「コールバックの中に処理を書いてそのコールバックに更に処理を…」みたいな大量のインデントを生み出してしまうのでそれを回避できるのが良さそうです。
4番もかなり使いどころが多そうです。
サーバーへ複数のリクエストを送って全部返って来たら特定の処理をするという時がかなり便利になりそうです。
PromiseKitをインストール
インストールはCocoaPodsで行います。
target 'MyApp' do use_frameworks! pod 'PromiseKit' end
PromiseKitを使ってみる(then/done)
1番シンプルな使い方は下の通りです。
最初のブロック実行後、done内のブロックが実行されます。
import UIKit import PromiseKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() _ = Promise<String> { seal in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { seal.fulfill("DONE!") print(1) } }.done { value in print(2) print(value) } print(3) } }
ログを見ると各ブロックが順番に実行されている事が分かります。
複数の処理を順番に動かしたい時はthenを使います。
import UIKit import PromiseKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() _ = Promise<String> { seal in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { seal.fulfill("DONE!") print(1) } }.then { result -> Promise<String> in Promise<String> { seal in DispatchQueue.main.asyncAfter(deadline: .now() + 2) { print(2) seal.fulfill("DONE!!") } } }.done { value in print(2) print(value) } } }
PromiseKitの例外処理(catch)
エラーハンドリングはcatchメソッドを使います。
ここにエラー時の処理を書く事でエラーが起きた時のハンドリングをします。
例外の発生はrejectメソッドを使って通知します。
enum MyError: Error { case error1 case error2 } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() Promise<String> { seal in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { seal.reject(MyError.error1) print(1) } }.done { value in print(2) print(value) }.catch { error in print(error.localizedDescription) } print(3) } }
ログを見るとcatchのブロックが呼び出されている事が分かります。
ないはずですが、fulfillやrejectを複数呼び出すと一番最初のものだけが有効になります。
seal.fulfill("DONE!")
seal.reject(MyError.error1)
seal.reject(MyError.error2)
PromiseKitで並列処理(when)
複数の処理を並列実行する場合はwhenを使います。
whenにPromiseの配列を渡す事で並列に実行してくれます。
import UIKit import PromiseKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let promises = (0...2).map { i -> Promise<String> in Promise<String> { seal in DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) { seal.fulfill("DONE! \(i)") print(i) } } } when(resolved: promises).done { values in print(values) } } }
ログを見ると各処理実行後にdoneのブロックが呼ばれている事が分かります。
PromiseKitのwhenのエラーハンドリング
whenではcatchメソッドを使う事ができません。
代わりにdoneの中でエラーハンドリングをします。
doneの引数に成功かどうかの値が入っているので、下のようにswitch文などでエラーハンドリングをします。
import UIKit import PromiseKit enum MyError: Error { case error1 } class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let promises = (0...2).map { i -> Promise<String> in Promise<String> { seal in DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)) { if i == 1 { seal.reject(MyError.error1) } else { seal.fulfill("DONE! \(i)") } print(i) } } } when(resolved: promises).done { values in values.forEach { switch $0 { case .fulfilled(let value): print(value) case .rejected(let error): print(error) } } } } }
常に呼ばれるメソッド(ensure/finally, 旧always)
ensureメソッドはエラーが起きても起きなくても呼ばれます。
import UIKit import PromiseKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() _ = Promise<String> { seal in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { seal.fulfill("DONE") print(1) } }.done { value in print(2) print(value) }.ensure { print("ensure") } } }
似たメソッドでfinallyがあります。
これはensureとほぼ同じで、違いとしてはfinallyの後ろにはメソッドを繋げられない事やcatchの後ろにしか使えない事くらいです。
import UIKit import PromiseKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() Promise<String> { seal in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { seal.fulfill("DONE") print(1) } }.done { value in print(2) print(value) }.catch { error in print(error.localizedDescription) }.finally { print("finally") } } }
PromiseKitでUIViewのアニメーション
下のようにUIViewのanimationでPromiseKitを使う事も可能です。
import UIKit import PromiseKit class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let v = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100)) v.backgroundColor = UIColor.darkGray view.addSubview(v) UIView.animate(.promise, duration: 2.0) { v.frame.origin.x = 200 }.done { result in print(result) } } }
まとめ
PromiseKitはジェネリックスも多くて理解するのがかなり難しかったです。
しかし今回で基本的な使い方を勉強できたので機会あれば使っていきたいと思います。