Swiftの非同期処理を簡単に書けるBrightFuturesをコード例を多用して解説する

Swiftの非同期処理を簡単に書けるBrightFuturesをコード例を多用して解説するQiitaに書いた記事と同じ記事です。

Swiftの非同期処理を非常に簡単に書けるBrightFuturesですが、日本語の記事があまりないので、応援の意味も込めて書いてみようと思いました。

最新版である、3.1.2 の解説になります。

第一部:基本的な説明

簡単な例

READMEにもありますが、この例がまずわわかりやすいと思うので。

下記の例では、logInfetchPostsで非同期処理が行われます。

BrightFuturesを使わない場合

User.logIn(username, password) { user, error in // (A)
    if !error {
        Posts.fetchPosts(user, success: { posts in // (B)
            // do something with the user's posts
        }, failure: handleError)
    } else {
        handleError(error) // handeError is a custom function to handle errors
    }
}

BrightFuturesの場合

User.logIn(username,password).flatMap { user in // (A)
    Posts.fetchPosts(user) // (B)
}.onSuccess { posts in
    // do something with the user's posts
}.onFailure { error in
    // either logging in or fetching posts failed
}

BrightFuturesを使わない場合、非同期処理を直列で行った際に、複数階層のネストが生じてしまいます。失敗した場合の処理も、そのネストごとに記載する必要があります。比較してBrightFuturesを使った場合はそれを直列で書くことができます。

この場合、(A)の処理が成功した場合、(B)の処理を行います。(B)の処理が成功した場合、onSuccessが実行されます。

(A)の処理が失敗した場合、(B)以降の処理が行われず、onFailureが実行されます。(B)の時点で失敗した場合も、onFailureが実行されます。

flatMapを使ってもっと直列に

flatMapを使っていくつでも処理をつなげることができます。下記の例では、すべて成功した場合には、onSuccessが呼ばれて、date: 2015-11-29 18:21:42 +0000が出力されます。途中で失敗した場合は、onFailureが呼ばれて処理別に登録していたエラーerror: asyncIntが出力されます。

Sample.asyncString(1.0, success: true).flatMap { (string) -> Future<String, NSError> in
    return Sample.asyncString(1.0, success: true)
}.flatMap { (string) -> Future<Int, NSError> in
    return Sample.asyncInt(1.0, success: true)
}.flatMap { (string) -> Future<NSDate, NSError> in
    return Sample.asyncDate(1.0, success: true)
}.onSuccess { (date) -> Void in
    print("date: (date)") // date: 2015-11-29 18:21:42 +0000
}.onFailure { (error) -> Void in
    print("error: (error.localizedDescription)") // error: asyncInt
}

上記例で利用した非同期処理を次に説明します。

PromiseからFutureを作る

flatMapを使ってもっと直列にで説明した際に、返り値の異なる非同期処理を複数利用しました。それはこのように作成しています。

struct Sample {
    static func asyncString(interval: Double, success: Bool) -> Future<String, NSError> { // (A)
        let promise = Promise<String, NSError>() // (B)
        Queue.global.async { () -> Void in // (C)
            NSThread.sleepForTimeInterval(interval)
            if success {
                promise.success("Async success")
            } else {
                promise.failure(NSError.createError("asyncString"))
            }
        }
        return promise.future // (D)
    }
}

(B)の位置で、Promiseの型を指定し、そこにsuccessfailureをセットし、(D)の位置でpromiseからfutureを返しています。(B)で指定する型は関数の返却する型(A)と同じである必要があります。

Futureはすぐに返す必要があるのですが、上記、直列flatMapの例のように、一つ一つのfutureが成功するか失敗するかしたら次の処理が行われるという流れになります。

(C)の部分で利用しているQueueですが、これもBrightFuturesが提供してくれているGCDのラッパーです。便利なので利用していますが、本質からはそれるので深くは触れません。この部分では非同期で指定秒スリープした後に、指定された結果を返すようにしています。

ちなみに、Errorは説明のためにこのような簡単なextensionとしています。これはサンプルのためのコードなので解説しません。

extension NSError {
    static func createError(localizedDescription: String) -> NSError {
        let userInfo = [NSLocalizedDescriptionKey: localizedDescription]
        return NSError(domain: "sample", code: -1, userInfo: userInfo)
    }
}

andThenで副作用なく処理を挟む

下記の例では、Future<Int, NoError>(value: 4)は必ず成功して、andThenresultのenumは.Successとなります。この場合は値として4が入ってきます。

answer *= val で、answerは40となります。

次のandThenで渡ってくるresultも同様で、.Successとなり値は4です。

今回は値を使わず、 answer += 2 とするので、この時点でanswerは42となります。

このように、andThenでは結果に影響を与えずに処理を挟むことができます。

var answer = 10

let f = Future<Int, NoError>(value: 4).andThen { result in
    switch result {
    case .Success(let val):
        answer *= val
    case .Failure(_):
        break
    }
}.andThen { result in
    if case .Success(_) = result {
        answer += 2
    }
}

実行スレッドを変える

Futureは必ず、CGDでいう global queue で処理されるので、画面の更新などを行う際は、contextを変えるのが良いと思います。

Sample.asyncString(1.0, success: true).flatMap { (string) -> Future<String, NSError> in
    return Sample.asyncString(1.0, success: true)
}.onSuccess(Queue.main.context) { [weak self] (string) -> Void in
    self?.label.text = string
}

ここまでで、BrightFuturesは使えるようになります。初めての場合は一旦この辺りまでで動かしてみると良いかもしれません。

第2部: もう少し深く

recoverで失敗時の値を指定する

上記の例を一部改変して、途中で失敗させます。

<

pre class=”brush:swift”>
Sample.asyncString(1.0, success: true).flatMap { (string) -> Future<String, NSError> in
return Sample.asyncString(1.0, success: false)
}.onSuccess { (date) -> Void in
print(“date: (date)”)
}.onFailure { (error) -> Void in
print(“error: (error.localizedDescription)”) // error: asyncString
}
“`

この場合、error: asyncStringが出力されます。この時に、失敗したとしても、とりあえずNo dataという文字列を返したいとします。その際はこのように書けます。

Sample.asyncString(1.0, success: true).flatMap { (string) -> Future<String, NSError> in
    return Sample.asyncString(1.0, success: false)
}.recover { (error) -> String in
    return "No data"
}.onSuccess { (string) -> Void in
    print("string: (string)") // string: No data
}

mapで値を変更する

map はよく使うかもしれません。受け取った値を改変して返します。

// map
Sample.asyncString(1.0, success: true).flatMap { (string) -> Future<String, NSError> in
    return Sample.asyncString(1.0, success: true)
}.map { (string) -> Int in
    return 1
}.onSuccess { (number) -> Void in
    print("number: (number)") // number: 1
}

zipで値をまとめる

それぞれのFutureの成功の結果がonSuccessに入ってきます。

// zip
Sample.asyncString(1.0, success: true).zip(Sample.asyncString(1.0, success: true))
.onSuccess { (string1, string2) -> Void in
    print("1: (string1), 2: (string2)") // 1: Async success, 2: Async success
}

filterで成功の条件を決める

future関数は、この場合は必ず引数の値をとって成功します。そして、onCompleteonSuccessonFailureをまとめたものです。下記の、resultには成功のケースと失敗のケースのどちらかが入ってきます。

future(3).filter { $0 > 5 }.onComplete { result in
    // failed with error NoSuchElementError
}

future("Swift").filter { $0.hasPrefix("Sw") }.onComplete { result in
    // succeeded with value "Swift"
}

上の場合は3 > 5はfalseになるので失敗します。下の場合は、SwiftはSwから始まるので条件を満たし、成功します。

sequenceで一斉に実行する

すべてが成功したら、onSuccessが実行されます。その場合、resultはすべての成功の結果が配列として渡ってきます。

let sequence = [
    Sample.asyncString(1.0, success: true),
    Sample.asyncString(3.0, success: true),
    Sample.asyncString(4.0, success: true),
    Sample.asyncString(1.0, success: true),
]
sequence.sequence().onSuccess { (results) -> Void in
    print(results) // ["Async success", "Async success", "Async success", "Async success"]
}

Invalidation tokenでcallbackを受け取らないようにする。

画像の非同期呼び出しや、早いペースでのページングなど、すでに不要になった処理の実行は止めたいもの。そういう場合に、Invalidation tokenが使えます。これは再利用されるセルへの画像のセット処理の例です。再利用された後に、以前、していた通信の結果が反映されてしまうのを防ぐことができます。

class MyCell : UICollectionViewCell {
    var token = InvalidationToken()

    public override func prepareForReuse() {
        super.prepareForReuse()
        token.invalidate()
        token = InvalidationToken()
    }

    public func setModel(model: Model) {
        ImageLoader.loadImage(model.image).onSuccess(token.validContext) { [weak self] UIImage in
            self.imageView.image = UIImage
        }
    }
}

この記事は主に、BrightFuturesのREADMEを基にしています。ドキュメントにはさらにもう少し説明があります。ドキュメント化されていないその他の機能もタイミングがあれば書いてみたいと思います。

BrightFuturesは関数型の考え方も自然に取り入れたとても扱いやすい非同期処理のライブラリです。これをきっかけに少しでも使う人が増えれば良いなと思います(スターももう少しついても良いと思っています)。

BrightFutures

関連記事

Pocket
LINEで送る

You may also like...