[Swift] BrightFuturesで非同期処理をflatMapでつなげる方法

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

はじめに

こんにちは!
モバイルアプリサービス部の田中孝明です。

こちらのブログの続きのエントリーになります。
Swift 3.0に移行する上で、従来の機能をおさらいしておこうという内容のブログになります。

Future と Promise

Java、Scala、JavaScriptを実装されて事ある方には馴染み深い非同期処理の機構です。
Futureは未来の結果を表し、Promiseは成功と失敗を表す処理や値をFutureに変換する事ができます。
詳しくはFuture と Promiseを参照していただければ幸いです。

SwiftとFuture/Promise

Swiftには残念ながらFuture/Promiseの機構は備わっていません。
しかし、BrightFuturesというライブラリを組み込むことで、Future/Promiseを利用する事ができます。

導入方法に関しては弊社のブログ、[Swift] 非同期処理フレームワークBrightFutures ~導入編~が参考になると思います。
内容がswift3.0以前のものなので、復習も兼ねて書き記していきます。

導入

API通信をFutureで非同期処理するサンプルを作りたいと思います。
今回もCocoaPodsで導入します。

pod 'Alamofire', '~> 4.0.1'
pod 'SwiftyJSON'
pod 'BrightFutures'

post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['SWIFT_VERSION'] = '3.0'
        end
    end
end

AlamofireSwiftyJSONに関してはAPI通信の際に利用するために導入しています。
FuturePromiseを利用する場合はBrightFuturesのみで十分です。

モックの作成

ローカルの環境で動作テストするだけですのでjson-serverを利用してJSONを返すだけのモックを用意します。
モック用のjsonファイルは以下のようなものを用意しました。

{
  "users": {
    "id": 1,
    "name": "test-1",
    "token": "XXXXXXXXXXXXX"
  },
  "profile": {
    "firstname": "TAKAAKI",
    "lastname": "TANAKA"
  },
  "messages": [
    {
      "id": 1,
      "text": "hello"
    },
    {
      "id": 2,
      "text": "nice"
    }
  ]
}

それぞれusersprofilemessagesでアクセスした際に定義された以下のJSONを返却する動作になります。
json-serverはPOSTも対応していますが、今回はGETのみで利用します。

iOSアプリ側でJSONの結果を受け取ってパースした結果を保持するstructを定義しておきます。

struct User {
    let id: Int?
    let name: String?
    let token: String?

    init(json: JSON) {
        id = json["id"].int
        name = json["name"].string
        token = json["token"].string
    }
}

struct Profile {
    let firstname: String?
    let lastname: String?

    init(json: JSON) {
        firstname = json["firstname"].string
        lastname = json["lastname"].string
    }
}

struct Message {
    let id: Int?
    let text: String?

    init(json: JSON) {
        id = json["id"].int
        text = json["text"].string
    }
}

Future/Promiseを適用しない場合

クロージャを利用して結果を受け取るようにすることが多いのでは無いでしょうか。

var usersComplitionHandler: ((User) -> Void)?
var profileComplitionHandler: ((Profile) -> Void)?
var messagesComplitionHandler: (([Message]) -> Void)?
// すごく雑な作り方

usersComplitionHandler = { user in
    print(user)
}

Alamofire.request("http://localhost:3000/users").responseJSON { [weak self] response in
    switch response.result {
    case .success(let value):
        let json = JSON(value)
        let user = User(json: json)
        self?.usersComplitionHandler?(user)
    case .failure(let error):
        print(error)
    }
}

Future/Promiseを適用した場合

usersの処理を置き換えてみます。
リクエストの成功をpromise.successに、失敗をpromise.failureに定義します。

promise.futureでFutuerに変換したものを返却します。

private func getUser() -> Future<User, NSError> {
    let promise = Promise<User, NSError>()
    let queue = DispatchQueue(label: "getUser", attributes: .concurrent)
    Alamofire.request("http://localhost:3000/users").responseJSON(queue: queue, completionHandler: { response in
        switch response.result {
        case .success(let value):
            let json = JSON(value)
            let user = User(json: json)
            promise.success(user)
        case .failure(let error):
            print(error)
            promise.failure(error as NSError)
        }
    })
    return promise.future
}

あとは呼び出し元で成功時の処理をonSuccessと失敗時の処理をonFailureに定義します。
Futureの名前が示す通り、受け取り側は「いずれ結果が返ってくるもの」に対して成功と失敗の処理を書きます。

let userFuture = getUser()

userFuture.onSuccess { user in
    print(user)
}.onFailure { error in
    print(error)
}

どちらの処理が書きやすいかは主観の域を出ないので、ここでは言及しません。

複数の非同期処理を直列に行う

Future/Promiseを適用しない

Future/Promiseを適用しないで、2つ以上の非同期処理を直列で呼びたい時は、クロージャ内でクロージャを呼ぶなど、多少煩雑になったりします。
試しにusersprofilemessagesを順番に呼び出す処理を記します。

// 絶対真似してはダメ

usersComplitionHandler = { [weak self] user in   
    Alamofire.request("http://localhost:3000/profile").responseJSON { [weak self] response in
        switch response.result {
        case .success(let value):
            let json = JSON(value)
            let profile = Profile(json: json)
            self?.profileComplitionHandler?(profile)
        case .failure(let error):
            print(error)
        }
    }
}

profileComplitionHandler = { [weak self] profile in      
    Alamofire.request("http://localhost:3000/messages").responseJSON { [weak self] response in
        switch response.result {
        case .success(let value):
            let json = JSON(value)
            let messages = json.arrayValue.map { Message(json: $0) }
            self?.messagesComplitionHandler?(messages)
        case .failure(let error):
            print(error)
        }
    }    
}

messagesComplitionHandler = { [weak self] messages in
    print(messages)
}

Alamofire.request("http://localhost:3000/users").responseJSON { [weak self] response in
    switch response.result {
    case .success(let value):
        let json = JSON(value)
        let user = User(json: json)
        self?.usersComplitionHandler?(user)
    case .failure(let error):
        print(error)
    }
}        

クロージャの処理が絡むため、処理の順番が追いにくくなってしまいます。

Future/Promiseを適用

ではFuture/Promiseを適用した場合はどうでしょうか。
まずはusersと同様にprofilemessagesFutureで返すように定義します。

private func getProfile(id: Int?) -> Future<Profile, NSError> {
    let promise = Promise<Profile, NSError>()
    let queue = DispatchQueue(label: "getProfile", attributes: .concurrent)
    Alamofire.request("http://localhost:3000/profile").responseJSON(queue: queue, completionHandler: { response in
        switch response.result {
        case .success(let value):
            let json = JSON(value)
            let profile = Profile(json: json)
            promise.success(profile)
        case .failure(let error):
            print(error)
            promise.failure(error as NSError)
        }
    })
    return promise.future
}

private func getMessages(name: String?) -> Future<[Message], NSError> {
    let promise = Promise<[Message], NSError>()
    let queue = DispatchQueue(label: "getProfile", attributes: .concurrent)
    Alamofire.request("http://localhost:3000/messages").responseJSON(queue: queue, completionHandler: { response in
        switch response.result {
        case .success(let value):
            let json = JSON(value)
            let messages = json.arrayValue.map { Message(json: $0) }
            promise.success(messages)
        case .failure(let error):
            print(error)
            promise.failure(error as NSError)
        }
    })
    return promise.future
}

FutureにはflatMapが定義されているため、結果をflatMapで繋げて直列に処理する事ができます。

getUser().flatMap { user -> Future<Profile, NSError> in
    self.getProfile(id: user.id)
}.flatMap { profile -> Future<[Message], NSError> in
    self.getMessages(name: profile.name)
}.onSuccess { messages in
    print(messages)
}.onFailure { error in
    // 全てのAPIの失敗はこちらに集約される
    print(error)
}

それぞれのFutureに対してflatMapで繋げることで、非同期処理を順番に行なう事ができます。
onSuccessには最終的な非同期処理の成功の結果を、onFailureでは全ての非同期処理の失敗が処理されるようになります。
Future/Promiseを利用した事ある方には馴染みのある記法になったのではないでしょうか。
ただし、こちらのブログでも記載した通り、Swiftにはfor(yield)のようなシンタックスシュガーが提供されていないので、多少煩雑に感じるでしょう。

まとめ

Future/Promiseパターンを導入したからといって劇的に非同期処理が楽に書けるようになるとは限らないですが、Java、Scala、JavaScriptで実装した事のある人に馴染みのある記法を使う事で可読性を上げる事ができるのではないでしょうか。
ぜひ標準でサポートしていただきたい機構ではあります。

参考文献

Future と Promise
[Swift] 非同期処理フレームワークBrightFutures ~導入編~
【Scala】flatMap は怖くない!
【Scala】Future と未来のセカイ