PromiseKitを使って非同期処理プログラミングのつらみと卒業しよう(応用編)

概要

前回の続きになります。

PromiseKitを使って非同期処理プログラミングのつらみと卒業しよう(導入編)

原文はこちらになります。

Getting Started With PromiseKit

Wrapping in a Promise

PromiseKitサポートが組み込まれていない既存のコード、SDKサードパーティのライブラリを使用するとどうなりますか?ええと、PromiseKitにはpromiseのラッパーが付いています。

たとえば、このアプリを取る。気象条件は限られているため、毎回ウェブから条件アイコンを取得する必要はありません。それは非効率的で潜在的にコスト大です。

WeatherHelper.swiftには、ローカルキャッシュディレクトリから画像ファイルを保存したり読み込んだりするためのヘルパー関数が既に用意されています。これらの関数はバックグラウンドスレッド上でファイルI/Oを実行し、オペレーションが終了すると非同期completionブロックを使用します。 これは一般的なパターンなので、PromiseKitには組み込みの処理方法があります。

WeatherHelpergetIcon(named :)を次のように置き換えます(欠落しているメソッドについてのコンパイラエラーは無視してください)。

WeatherHelper.swift
func getIcon(named iconName: String) -> Promise<UIImage> {
  return Promise<UIImage> {
    getFile(named: iconName, completion: $0.resolve) // 1
  }
  .recover { _ in // 2
    self.getIconFromNetwork(named: iconName)
  }
}

このコードの仕組みは次のとおりです。

  1. promiseを以前と同じように構築しますが、1つだけ小さな違いがあります。promiseのfulfillrejectする代わりにresolveメソッドを使用します。getFile(named:completion :)のcompletionクロージャシグネチャresolveメソッドのシグネチャと一致するため、その参照を渡すと、指定されたcompletionクロージャのすべての結果のケースを自動的に処理します。
  2. アイコンがローカルに存在しない場合、recoverクロージャが実行され、別のpromiseを使用してネットワーク経由でフェッチします。

valueで作成されたpromiseが実行されない場合、PromiseKitはそのrecoverクロージャを呼び出します。それ以外の場合、画像がすでにロードされていて、すぐに使用できる場合は、recoverを呼び出さずにすぐに戻ることができます。このパターンは、非同期的(ネットワークからのロードのように)だったり、同期的(メモリ内のvalueのように)だったりですが何かを行うことができるpromiseをどのように作成するかです。これは、イメージなど、ローカルにキャッシュされた値を持つ場合に便利です。

このようにするには、画像が入ったときに画像をキャッシュに保存する必要があります。前の方法の下で次のように追加します。

WeatherHelper.swift
func getIconFromNetwork(named iconName: String) -> Promise<UIImage> {
  let urlString = "http://openweathermap.org/img/w/\(iconName).png"
  let url = URL(string: urlString)!

  return firstly {
    URLSession.shared.dataTask(.promise, with: url)
  }
  .then(on: DispatchQueue.global(qos: .background)) { urlResponse in
    return Promise {
      self.saveFile(named: iconName, data: urlResponse.data, completion: $0.resolve)
    }
    .then(on: DispatchQueue.global(qos: .background)) {
      return Promise.value(UIImage(data: urlResponse.data)!)
    }
  }
}

これは、dataPromisethenブロックをのぞいて以前のgetIcon(named:)と似ていますがgetFileの場合にした時と同様にラップするsaveFileの呼び出しがあります。

これはfirstlyと呼ばれるコンストラクトを使用します。 firstlyは単にそのpromiseを実行するシンタックスシュガーです。読みやすさのために間接参照のレイヤーを追加する以外には、実際には何もしません。saveFileを呼び出すことは、アイコンの読み込みの副作用であるため、ここではfirstlyで少しだけorderingを強制します。

あなたがアイコンを最初に要求したときに起こることは、すべてここにあります。

  1. まず、アイコンのURLSessionリクエストを行います。
  2. 完了したら、データをファイルに保存します。
  3. イメージをローカルに保存したら、データをイメージに変換してチェーンに送ります。

今すぐビルドして実行すると、アプリの機能に違いは見られませんが、ファイルシステムをチェックして画像がローカルに保存されていることを確認できます。これを行うには、コンソール出力から「Saved image to:」という単語を検索します。 これにより、新しいファイルのURLが表示されます。このURLを使用してディスク上の場所を見つけることができます。

5_filesystem_with_cached_images.png

Ensuring Actions

PromiseKitのシンタックスをみていると不思議に思うかもしれませんね。thencatchがある場合、コードを共有し、successまたはfailureに関係なく、アクションが(クリーンアップタスクのように)常に実行されるようにする方法がありますか?まあ、あります:それはfinallyに呼ばれます。

サーバーから天気を取得するためにPromiseを使用している間、ステータスバーにネットワークアクティビティインジケータを表示するためにWeatherViewController.swifthandleLocation(city:state:coordinate:)を更新しましょう。

weatherAPI.getWeather ...の呼び出しの前に次の行を挿入します。

WeatherViewController.swift
UIApplication.shared.isNetworkActivityIndicatorVisible = true

次に、次のものをcatchクロージャーの最後まで連鎖させます:

WeatherViewController.swift
.finally {
  UIApplication.shared.isNetworkActivityIndicatorVisible = false
}

これはfinallyに使用する標準的な例です。 天気が完全に読み込まれた場合でもエラーが発生した場合でも、ネットワーク活動を担当するPromiseは終了しますので、いつでもアクティビティインジケータを却下してください。 同様に、finallyを使用してソケットだったりデータベース接続を閉じたり、ハードウェアサービスから切断することができます。

Implementing Timers

1つの特別なケースは、あるデータが準備されているときではなく、一定の時間間隔が経過した後で実現されるPromiseです。 現在、アプリが天気を読み込んだ後、決して更新されません。 天気を毎時更新するように変更してください。

updateWithCurrentLocation()では、メソッドの最後に次のコードを追加します。

WeatherViewController.swift
after(seconds: oneHour).done { [weak self] in
  self?.updateWithCurrentLocation()
}

.after(seconds :)は指定された秒数が経過すると完了するpromiseを作成します。 残念ながら、これはワンショットタイマです。1時間ごとに更新を行うために、再帰的なupdateWithCurrentLocation()を作成しました。

much_later.png

Using Parallel Promises

これまで説明したすべてのpromiseは、独立しているか、あるいは連鎖しています。 PromiseKitは、複数のpromiseを並行して処理する機能も提供しています。 複数のpromiseを待つ2つの機能があります。最初のraceは、promiseのグループの最初のものが達成されたときに達成されるpromiseを返します。 本質的に、完了した最初のものが勝者です。

他の機能はwhenです。指定されたpromiseがすべて満たされた後でそれが実現します。when(fulfilled:)は、promiseのいずれかが行われるとすぐに拒絶で終わります。すべてのpromiseが完了するのを待っているwhen(resolved:)もありますが、常にthenブロックを呼び出し、決してcatchすることはありません。

これらのグループ化機能のすべてについて、すべての個々のpromiseは結合機能の動作に関係なく、それらが実行または拒否されるまで継続します。たとえば、あるraceで3つのpromiseを使用した場合、racethenクロージャは、最初のpromiseが完了した後に実行されます。しかし、他の2つの未完成のpromiseは解決するまで実行を続けます。

天気を「random」な都市で示すという、人為的な例を考えてみましょう。 ユーザーは表示する都市を気にしないので、アプリは複数の都市の天気を取得しようと試みることができますが、最初のものを処理するだけで完了します。これがランダム性の錯覚を与えます。

showRandomWeather(_ :)を次のように置き換えます。

WeatherViewController.swift
@IBAction func showRandomWeather(_ sender: AnyObject) {
  randomWeatherButton.isEnabled = false

  let weatherPromises = randomCities.map { 
    weatherAPI.getWeather(atLatitude: $0.2, longitude: $0.3)
  }

  UIApplication.shared.isNetworkActivityIndicatorVisible = true

  race(weatherPromises)
    .then { [weak self] weatherInfo -> Promise<UIImage> in
      guard let self = self else { return brokenPromise() }

      self.placeLabel.text = weatherInfo.name
      self.updateUI(with: weatherInfo)
      return self.weatherAPI.getIcon(named: weatherInfo.weather.first!.icon)
    }
    .done { icon in
      self.iconImageView.image = icon
    }
    .catch { error in
      self.tempLabel.text = "--"
      self.conditionLabel.text = error.localizedDescription
      self.conditionLabel.textColor = errorColor
    }
    .finally {
      UIApplication.shared.isNetworkActivityIndicatorVisible = false
      self.randomWeatherButton.isEnabled = true
    }
}

ここでは、都市の選択のために天気を取得するPromiseの束を作成します。これらはrace(promises:)でお互いが競り合います。thenクロージャは、それらのpromiseのうちの最初のものがfulfillしたときにのみ実行されます。doneブロックはimageを更新します。エラーが発生するとcatchクロージャがUI clean upを処理します。最後に、残ったfinallyがあなたのアクティビティインジケータがクリアされ、ボタンが再び有効になるようにします。

理論的には、これはサーバーの条件の変化によってランダムに選択する必要がありますが、それは良い例えではありません。またpromiseはまだすべて解決されるので、あなたはただ1つのことにだけ気をつけますが、まだ5つのネットワーク要求があることにも注意してください。

ビルドして実行します。アプリが読み込まれたら、Random Weatherをタップします。

6_random_weather.png

参考単語帳

  • wrapper : ラッパー
  • delegate: デリゲート。デザインパターンで使われるもの
  • asynchronicity: 非同期性
  • execute: 実行する
  • fulfill: 果たす