これはなに
入門編です。
非同期処理を実現するためには closure (Objective-C なら blocks) や delegate、observer などがあると思いますが、
これらを Rx で置き換えましょう。
なお、比較対象として closure と RxSwift に絞ります。これは私の体感として、Swift ではだいたい closure を使う場面が多いかなと思うからです。
環境
- Xcode 7.1.1 (Swift 2.1)
- Playground
- RxSwift 2.0.0-beta.3
実現したいこと
ある整数を引数に渡したら非同期でその数を10倍して返してくれる 構造体A があり、それの結果をいろいろいじりたい、そういうのを考えます。
closure でやるといわゆる「コールバック地獄」に陥る事態も Rx ならばスッキリ書ける、という可読性向上を狙います。
モナドとか、関数型とか、そういう計算科学的なことは扱いません。
closure おさらい
上記のことを closure で実現しようとすると以下になります。
struct closureA {
func request(num: Int, response: (Int) -> ()) {
response(num * 10)
}
}
let a = closureA()
a.request(1) {
print($0) // 10
}
a.request(5) {
print($0) // 50
}
RxSwift で書く
同じように RxSwift を使って書いてみます。Observable<Element>
を使います。
struct observableA {
func request(num: Int) -> Observable<Int> {
return just(num * 10)
}
}
let a = observableA()
a.request(1)
.subscribeNext {
print($0) // 10
}
.dispose()
a.request(5)
.subscribeNext {
print($0) // 50
}
.dispose()
just(element: Element) -> Observable<Element>
であり、ここで Observable を生成しています。
そして subscribeNext
で成功した時の値を取り出しています。
これならまだ closure で書いたほうがいいですね。
返ってきた結果を整理したい
Int
で返ってきたものを String
に変換する
let closure = closureA()
closure.request(1) {
let str = String($0)
print(str) // "10"
}
let observable = observableA()
observable.request(1)
.map { String($0) }
.subscribeNext {
print($0) // "10"
}
.dispose()
1以上の自然数だけを String
に変換して出力したい
let closure = closureA()
closure.request(1) { num in
guard num > 0 else {
return
}
let str = String(num)
print(str) // "10"
}
let observable = observableA()
observable.request(1)
.filter { $0 > 0 }
.map { String($0) }
.subscribeNext {
print($0)
}
.dispose()
…どうでしょう?後処理が多くなればなるほど closure の中身が肥大化していきますね。すると本当に欲しいものがどこにあるか見つけづらくなっていくことかと思います。メソッドに切り出して後処理を投げるという手もありますがスコープを外れてしまうので可読性は?というところがあります。
一方 Rx の方は一番欲しいものである subscribeNext
の中身は一切変わることなく、filter
, map
(flatMap
もあります) でチェインすることで情報を整理することができます。
コールバック地獄
さて話を変えます。
実践的な非同期処理では、「ある API を叩いた結果を使って別の API を叩く」や、「複数の API を同時に叩く」といったことがあります。
特に前者のような場合では「closure の中に closure を記述する」ということになり、非常に階層が深くなってしまいます。
apiRequest.getUser("swift") { userData in
apiRequest.follow(userData.id) { result in
switch result {
case .Success:
print("follow success!")
case .Failure:
print("something is wrong...")
}
}
}
これを Rx の機能を使うとスッキリします。
apiRequest.getUser("swift")
.map { userData in
apiRequest.follow(userData.id)
}
.switchLatest()
.subscribe (
onNext: {
print("follow success!")
},
onError: {
print("something is wrong...")
},
onCompleted: nil,
onDisposed: nil
)
.dispose()
エラーかどうかの分岐もわざわざ switch
を使わなくても subscribe
の onNext:
, onError:
に振り分けることができるのも便利です。
実践編(Alamofire を Rx にしてみよう)
Swift のネットワークライブラリではもはやデファクトと言ってよい Alamofire。
これは標準では closure で結果を返すようになっています。
Alamofire.request(.GET, "https://httpbin.org/get")
.responseJSON { response in
debugPrint(response)
}
(README より)
これを Observable で返すようにします。
まずはリクエストのためのインターフェースを切ります。
func call(method: Alamofire.Method, url: String) -> Observable<AnyObject>
<AnyObject>
の部分は SwiftyJSON や Argo を使って JSON
型にしてもよいでしょう。
実装します。
func call(method: Alamofire.Method, url: String) -> Observable<AnyObject> {
return create { observer -> Disposable in
Alamofire.request(method, url)
.responseJSON { response in
switch response.result {
case .Success(let value):
observer.on(.Next(value))
observer.on(.Completed)
case .Failure(let error):
observer.on(.Error(error))
}
}
return AnonymousDisposable { }
}
}
これで HTTP リクエストで Rx を使うことができます。
まとめ
このように Rx を取り入れることでより読みやすいコードを書いていきましょう。