しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

SwiftでPromiseパターンを実現するPromiseKitを使ってみる (Swift4 & PromiseKit6対応)

Promiseパターンでできること

Promiseパターンを使うと何ができるのかを調べてみました。

  1. 非同期で処理を行い、処理終了後に特定のメソッドを呼び出す
  2. 失敗時の処理を登録しておくことで、最初の処理が失敗した時にそのメソッドが呼ばれる
  3. 複数の非同期処理を順番に実行する
  4. 複数の非同期処理を並行に実行して、全て成功したタイミングでコールバックメソッドを呼ぶ

プロミスパターンでは上の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)
    }
}

ログを見ると各ブロックが順番に実行されている事が分かります。

f:id:llcc:20180703140057p:plain

複数の処理を順番に動かしたい時は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のブロックが呼び出されている事が分かります。

f:id:llcc:20180703140710p:plain

ないはずですが、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のブロックが呼ばれている事が分かります。

f:id:llcc:20180703143807p:plain

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)
                }
            }
        }
    }
}

f:id:llcc:20180703145625p:plain

常に呼ばれるメソッド(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はジェネリックスも多くて理解するのがかなり難しかったです。
しかし今回で基本的な使い方を勉強できたので機会あれば使っていきたいと思います。

参考URL

非同期処理とPromise(Deferred)を背景から理解しよう - hifive Redirecting...