Takahiro Octopress Blog

-1から始める情弱プログラミング

PromiseKitを使ってみよう!(2)

| Comments

はじめに

今回は久しぶりにPromiseKitについて書きたいと思います。
今から約2年前にPromiseKitを使ってみよう!で少し触れていたのですが、
最近業務で扱うことも増えてきたので改めて書き留めておこうと思います。

Promiseとは

PromiseKit に記載されている内容を訳すと(若干、意訳してますが…)、

  • シンプルな非同期プログラミングを実現できる
  • 上記によって、開発者がより重要な課題に集中することができる
  • 学習ハードルが低いため、マスターすることが簡単である
  • 可読性の高いコードが実現できるため、チーム開発にも向いている

といった感じです。
これを見る限り、非常に期待できますよね。
では、具体的な使い方を見ていきましょう。

アラートでの利用例

UIAlertViewController を利用した時に、通常は以下のように completion を引数に持って実装するかと思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
internal func showConfirm(title: String,
                          message: String,
                          okCompletion: @escaping (() -> Void),
                          cancelCompletion: @escaping (() -> Void)) {
    let alert = UIAlertController.init(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
    let okAction = UIAlertAction.init(title: "OK", style: UIAlertActionStyle.default) { _ in
        okCompletion()
    }
    let cancelAction = UIAlertAction.init(title: "キャンセル", style: UIAlertActionStyle.cancel) { _ in
        cancelCompletion()
    }
    alert.addAction(okAction)
    alert.addAction(cancelAction)
    present(alert, animated: true, completion: nil)
}

これだと引数が多くなってしまいますし、呼び出し側でも下記のように書くことになります。

1
2
3
4
5
self.showConfirm(title: "確認", message: "OKとキャンセルどちらをタップしますか", okCompletion: {
    // OKボタンをクリックした場合に呼び出される
}) {
    // キャンセルボタンをクリックした場合に呼び出される
}

これを PromiseKit を使って書くとどうなるでしょうか?
まずは、アラートのメソッドは下記のように書けます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import PromiseKit

internal enum AlertError: Error {
    case cancel
}

internal func showConfirm(title: String, message: String) -> Promise<Void> {
    let (promise, resolver) = Promise<Void>.pending()

    let alert = UIAlertController.init(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
    let okAction = UIAlertAction.init(title: R.string.common.ok(), style: UIAlertActionStyle.default) { _ in
        resolver.fulfill(Void())
    }
    let cancelAction = UIAlertAction.init(title: R.string.common.cancel(), style: UIAlertActionStyle.cancel) { _ in
        resolver.reject(AlertError.cancel)
    }
    alert.addAction(okAction)
    alert.addAction(cancelAction)
    present(alert, animated: true, completion: nil)

    return promise
}

呼び出し側では、

1
2
3
4
5
6
7
8
9
import PromiseKit

firstly {
  showConfirm(title: "確認", message: "OKとキャンセルどちらをタップしますか")
}.done { _ in
    // OKをタップした場合に呼び出される処理
}.catch { [weak self] error in
    // キャンセルをタップした場合に呼び出される処理
}

と書くことができ、非常にわかりやすく後続の処理を書くことができます。

API呼び出しの利用例

続いて、API呼び出しの場合の利用例を見ていきましょう。
今回API通信処理では Moya を利用します。
また、叩くAPIは Google Places API を利用します。

まずは Moya の書き方ですが、

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import Foundation
import Moya
import PromiseKit

// APIエラーの定義
internal enum APIError: Error {
    case cancel
    case apiError(description: String)
    case decodeError
}

internal enum SampleAPITarget {
    case places(lat: Double, lng: Double)
}

extension SampleAPITarget: TargetType {

    /// API Key
    private var apiKey: String {
        // 以下はGoogleのAPIキーをkey.plistで保持していると仮定した処理です。
        guard let path = Bundle.main.path(forResource: "key", ofType: "plist") else {
            fatalError("key.plistが見つかりません")
        }

        guard let dic = NSDictionary(contentsOfFile: path) as? [String: Any] else {
            fatalError("key.plistの中身が想定通りではありません")
        }

        guard let apiKey = dic["googleApiKey"] as? String else {
            fatalError("Google APIのKeyが設定されていません")
        }

        return apiKey
    }

    // ベースURLを文字列で定義
    private var _baseURL: String {
        return "https://maps.googleapis.com/maps/api/place/nearbysearch/json"
    }

    public var baseURL: URL {
        return URL(string: _baseURL)!
    }

    // enumの値に対応したパスを指定
    public var path: String {
        switch self {
        case .places:
            return ""
        }
    }

    // enumの値に対応したHTTPメソッドを指定
    public var method: Moya.Method {
        switch self {
        case .places:
            return .get
        }
    }

    // スタブデータの設定
    public var sampleData: Data {
        switch self {
        case .places:
            return "Stub data".data(using: String.Encoding.utf8)!
        }
    }

    // パラメータの設定
    var task: Task {
        switch self {
        case .places(let lat, let lng):
            return .requestParameters(parameters: [
                R.string.common.keyFileName(): apiKey,
                R.string.common.locationKeyName(): "\(lat),\(lng)",
                R.string.common.radiusKeyName(): 500
                ], encoding: URLEncoding.default)
        }
    }

    // ヘッダーの設定
    var headers: [String: String]? {
        switch self {
        case .places:
            return nil
        }
    }
}

class SampleAPI {

    private var provider: MoyaProvider<SampleAPITarget>!

    /// イニシャライザ
    init() {
        provider = MoyaProvider<SampleAPITarget>()
    }
}

のように書きます。
( Moya はUnitテストが書きやすいので良いんですよね〜という話はまた後日… )

では、本題の PromiseKit を用いたAPI通信処理です。
先程の SampleAPI クラスにメソッドを追加してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SampleAPI {

    private var provider: MoyaProvider<SampleAPITarget>!

    /// イニシャライザ
    init() {
        provider = MoyaProvider<SampleAPITarget>()
    }

    // Placeの定義は
    // https://grandbig.github.io/blog/2018/04/23/codable-swift4-1/
    // の『オマケ:Google Places APIでCodableを利用する』を参照のこと
    func fetchPlaces(lat: Double, lng: Double) -> Promise<[Place]> {
        let (promise, resolver) = Promise<[Place]>.pending()

        provider.request(.places(lat: lat, lng: lng)) { result in
            switch result {
            case .success(let response):
                do {
                    let decoder = JSONDecoder()
                    decoder.keyDecodingStrategy = .convertFromSnakeCase
                    let places = try decoder.decode(Places.self, from: response.data)

                    resolver.fulfill(places.results)
                } catch {
                    resolver.reject(APIError.decodeError)
                }
            case .failure(let error):
                resolver.reject(APIError.apiError(description: error.localizedDescription))
            }
        }

        return promise
    }
}

上記のように定義できます。
これを呼び出すときは、

1
2
3
4
5
6
7
8
9
10
11
var worker = SampleAPI()

firstly {
    worker.fetchPlaces(lat: latitude, lng: longitude)
}.done { [weak self] results in
    guard let strongSelf = self else { return }
    print(results)
}.catch { [weak self] error in
    guard let strongSelf = self else { return }
    print(error.localizedDescription)
}

のように書くことができます。
こちらも縦に浅いネストで読むことができるため、可読性が高いと言えますね。

PromiseKitの公式Readme

PromiseKit のドキュメントは非常に丁寧なので、これを読むだけでも理解を相当深められると思います。

を読んでおけばOKかと。
(上記の話も別機会でかければと思います。)

まとめ

さて、如何でしたでしょうか?
筆者は割りと completion で書くのが好きだったのですが、 PromiseKit に慣れていくと、その良さにどんどん気づいていくことができました。
リトライ処理は遅延実行も簡単に対応できたりするので、ぜひ使ってみることをオススメします。
と言ったところで本日はここまで。

Comments