[Swift] 非同期処理フレームワークBrightFutures ~導入編~

2015.05.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Swiftでの非同期処理

iOSでの非同期処理のハンドリングには様々な方法があり、フレームワークの使用者は

  • delegate
  • KVO
  • NSNotificationCenter
  • Blocks
  • GrandCentralDispatch

などの手段を用いて時間をまたいだコールバック処理を実現していくことになります。

これらのObjective-Cで用いられていたしくみをそのまま使っても非同期処理は実現できるのですが、非同期に特徴的なボイラープレートコードを繰り返し書くことになります:

typealias DataHandler = ((NSData?, NSError?) -> ())

func dataWithError(handler: DataHandler) {
    let url = "http://sample.jp/data"
    let request = NSURLRequest(URL: NSURL(string: url)!)
    NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue())
    { response, data, error in
        if error != nil {
            handler(nil, error)
        } else {
            handler(data, nil)
        }
    }
}

このようなエラー付き非同期ハンドラはBlocksを用いた通信処理に頻繁に現れますが、エラーハンドリングについては定型的な処理を書くことが多く、非同期処理メソッドをラップするたびに違う型をもったハンドラのタイプエイリアスを書かなくてはならないです。

また、Swiftはenumを用いて計算が成功した状態とエラーの状態のどちらかを取るようなデータ型を次のように定義できます。

class Box<T> {
  let val: T
  init(_ val: T) {
    self.val = val
  }
}

enum Result<T> {
  case Success(Box<T>)
  case Failure(NSError)
}

このResult型は今回紹介するフレームワークにも含まれるものですが、成功すればResult.Successとして中身の型を返し、失敗すればResult.Failureとして中身のNSErrorを表すようなenumです。

尚、enumと型パラメータを併用するときには2015/5/18現在Swift1.2ではclass Boxに型パラメータをくるまないとコンパイルエラーを引き起こす為に、このような書きかたをします。

このようにOptionalのような「値がないかもしれない型」と類似して「エラーを持ちうるかもしれない型」を定義できるため、先ほど言及した旧来のObjective-Cの非同期エラーハンドリングはSwiftを使えばもっと扱いやすく書けます。

さらに、非同期処理の成功後に更に別の非同期処理を行いたい場合はネストが深くなり、意味のあるまとまりを持ったものとしてコードが追いにくくなります。

今回紹介するBrightFuturesはこれらの旧来の非同期処理に伴う問題点を解消します。

BrightFutures

BrightFuturesは非同期処理をFuture&Promiseパターンを使って扱うために用意されたSwift用フレームワークです。

Blocks等を用いて一回のみ値が帰ってくることが保証されている非同期処理や、NSErrorを用いたエラーハンドリングをより扱いやすくします。

BrightFuturesのゴールはSwiftの標準APIにコピー&ペーストでFuture&Promiseパターンの実装として組み込まれることです。その目標からか現状のSwiftの標準APIインターフェイスに合わせてメソッド名や引数名が定義されているように実際に使っていて感じています。

Futureについて簡単に記述してみます。BrightFuturesの代表的なクラスFutureは前節で説明されたResultをオプショナルとして次のように持ちます。

class Future<T> {
    var result: Result<T>?
}

非同期処理の結果が帰ってくるまではresultはnilであり、結果そのものの値がない状態です。一方、非同期処理の結果が返って来た時にはresultはResult型の値を中にもち、処理が成功したかどうかでさらにSuccess, Failureと2つの状態をとりえます。

結果的にFutureの内部状態は次の3つになります

  • 非同期処理処理中
  • 非同期処理が完了し、処理が成功した
  • 非同期処理が完了し、処理が失敗した

Figure1

BrightFuturesの使用例について見てみましょう。

いまログインする機能を有するUserクラスでログインをかけた後にPostsクラスで与えられたユーザーに対する投稿履歴を取得したいとしましょう。従来のエラーハンドリングだと以下の様なコードになると思われます。

User.logIn(username, password) { user, error in
    if !error {
        Posts.fetchPosts(user, success: { posts in
            // 取得した投稿履歴を使った処理
        }, failure: handleError)
    } else {
        // handleError関数はなにかエラーハンドリングを行う関数
        handleError(error)
    }
}

ユーザーとして非同期でログインした後に別の投稿取得処理が非同期でネストを深くして書かれています。また、エラーハンドリングに関してもhandlerError関数がいたる箇所で使われています。

これと同じような機能をBrightFuturesを用いて実装すると以下のようなコードになります。

User.logIn(username,password).flatMap { user in
    Posts.fetchPosts(user)
}.onSuccess { posts in
    // 取得した投稿履歴を使った処理
}.onFailure { error in
    // エラーハンドリングを一箇所で行う
}

この場合loginメソッドやfetchPostsメソッドは各々Future<User>, Future<[Post]>を即座に返すように実装されています。loginとfetchPostsがともに成功した時のみonSuccessのなかの処理が走り、一箇所でも失敗すると以降の処理は行われず、失敗内容のNSErrorとともにonFailureの中の処理が走ります。

非同期処理の後の非同期処理でネストを使うことはなくなり、更に様々な箇所で行われていたエラー処理も一箇所にまとめて行われます。

flatMapメソッド、onSuccessメソッド、onFailureメソッドの詳しい意味は本シリーズ詳細編で記述しますが、この例だけ見てもコードが簡潔になったと感じるのではないでしょうか。

導入方法

ここではiOS8以降でCocoapodsを用いて導入する方法について記述します。

まずCocoapodsを用い、プロジェクトファイルと同階層のPodfileに以下のように記述します。

use_frameworks!

pod 'BrightFutures'

ここでuse_frameworks!と宣言しているのはライブラリを動的フレームワークとしてインポートする意味を持ちます。

この後、

pod install

でライブラリがインポートされ、xcworkspaceをひらいてライブラリを使いたいSwiftファイルの中で

import BrightFutures

と宣言するとトップレベルに宣言された諸クラスや関数を使えるようになります。

さて、いま試しに最初のコード例で宣言されたdataWithError(handler: DataHandlder)関数をBrightFuturesのFutureを使って書き換えてみます。

import BrightFutures

func futureData() -> Future<NSData> {
    let url = "http://sample.jp/data"
    let request = NSURLRequest(URL: NSURL(string: url)!)
    let promise = Promise<NSData>()
    NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue())
    { response, data, error in
        if error != nil {
            promise.failure(error)
        } else {
            promise.success(data)
        }
    }
    return promise.future
}

煩わしい非同期ハンドラのタイプエイリアス宣言からはまず解放されます。またFutureとして非同期オブジェクトが即時返されますが、この中身について非同期で取得された値に応じた処理を書きたいときにはonComplete, onSuccess, onFailureなどのメソッド処理を別途書いていくことになります。また別の処理を繋げたいときにはflatMap等を用いていきますが、これらの詳細については本シリーズ詳細編で詳しく記述します。

参考サイト