これまでは問題なかったんですが、機能の追加でAPIリクエストを全部待ってから次の処理をしたくなりました。なのでPromise系のライブラリを導入して、きちんと待機することにしました。

Promise系のライブラリはいくつかあるようです。

代表格はスター数、10,000越えのPromiseKitか。

mxcl/PromiseKit
Promises for Swift & ObjC. Contribute to mxcl/PromiseKit development by creating an account on GitHub.

でもREADMEを読んでしっくり来たのはこっちだったので、Hydraというのを使いました。

malcommac/Hydra
Lightweight full-featured Promises, Async & Await Library in Swift - malcommac/Hydra

まず既存コード

かなりメタ化しちゃってますが、既存コードはこんな感じでした。

func reload() {
  API.getData1() { data1 in
    if let data = data1 {
      Data.setData1(data)
    }
  }

  API.getData2() { data2 in
    if let data = data2 {
      Data.setData2(data)
    }
  }
}

元々はこれで問題ありませんでした。ですが、全てのAPIが返ってきたあと次のページへ遷移するケースが発生しました。

きちんと全API呼び出しを待たないと、データが半端な状態でページ遷移をしてしまうことになります。Promiseの出番です。

Hydraを導入する

cocoapodsで導入しました。プロジェクトがSwift3系だったので、pod 'HydraAsync', '~> 1.0.2'して導入します。

API呼び出しを書き換える

元々のコードを次のように分離します。

import Hydra

func reload() {
  let promise1 = getData1()
  let promise2 = getData2()
}

func getData1() -> Promise<Void> {
  return Promise<Void>(in: .background, token: nil) { (resolve, reject, _) in
    API.getData1() { data1 in
      if let data = data1 {
        Data.setData1(data)
        resolve()
      } else {
        reject("error")
      }
    }
  }
}

func getData2() -> Promise<Void> {
  // 略...
}

Promise<Void>の最初の引数はiOSのDispatchQueue関連に対応するenumです。多くは.backgroundになるんじゃないかな。

2番目はInvalidationTokenでキャンセルするときに使うっぽい。今回は要らないから使ってなくて、よくわかってない。今回はnilにしてます。

最後のBlockがPromiseの実装を書くところ。

READMEのCreate a Promiseを参考に、元々のAPI呼び出しを移植してresolverejectを入れました。

今回はこの箇所でのみPromiseを使いたかっただけなので、API呼び出し自体をPromiseを返す実装に変更してしまうのはやめました。他の箇所への影響を考えてのことです。

逆にいうとAPI呼び出ししてPromiseを返す関数を1個挟むだけで使えるようになるので、小さく試すには便利。

完了を待つ

これはドキュメントのallの項目のとおり。

Hydra#all

さっきの例だと、

func reload() {
  let promises = [getData1(), getData2()]
  all(promises).then { _ in
    self.doSomething()
  }.catch { err in
  }
}

こうなります。すっきり書けました。