53ningen.com

@gomi_ningen's Website

ご注文はRxですか?

この記事は コミックマーケット 89 にて頒布された「ななか Inside Press vol.8」に寄稿した記事です。内容はだいぶ古いですがアーカイブとして、ここに残しておきます。

ゴミ人間(@gomi_ningen)です。2014 年春からラビットハウスで住み込みバイトを始めました。 同僚のリゼちゃんや可愛い妹のチノちゃんと楽しく働いています ♪ 昨年までは JVM と戯れるお仕事をしていたのですが、最近は専ら iOS アプリ開発に携わっています。

さて、今回はネイティブアプリ開発で採用が増えている、Rx 系ライブラリを利用した設計・開発の実践例をご紹介します。 本記事中では RxSwift/RxCocoa 2.0.0-beta4 を用いて説明を行いますが、それぞれ各プラットフォームの対応するライブラリに読み替えていただければ幸いです。

第 1 羽 ココア先輩の優雅な Rx チュートリアル

RxSwift を利用したアプリケーションの設計・開発の話に入る前に、そもそもどのようなライブラリなのか、ココア先輩と一緒に学んでいきましょう。 なお、もうすでに Rx 系のライブラリの利用に慣れている方は、第 1 羽を読み飛ばしても大丈夫です。

ココア先輩と行うライブラリの導入

RxSwift/RxCocoa の導入には CocoaPods が便利です。 CocoaPods は Bundler を用いて、以下のように導入することができます。

# プロジェクトルートに移動後、以下を実行 $ echo 'gem '\''cocoapods'\'', '\''~> 0.39.0'\''' >> Gemfile $ bundle install $ pod init

続いて pod init で生成された Podfile に以下の 2 行を追加し、bundle exec pod install すればライブラリの導入は完了です。

pod 'RxSwift', '2.0.0-beta4' pod 'RxCocoa', '2.0.0-beta4'

なお、筆者の動作確認環境は Xcode 7.2 になります。

Hello, world

Rx については色々な説明がありますが、今回はまず動くコードを見ていくことにします。 ド定番の Hello, world は RxSwift を import した上で次のように書きます。

["hello, world"].asObservable() .subscribeNext({ (str) -> Void in NSLog(str) })

これを実行すると無事 "Hello, world" が出力されると思います。 また asObservable() の前の配列に要素を追加すると、各要素が NSLog されると思うので試してみてください。 どうやら配列の要素が次々と subscribeNext の引数 str に渡され、実行されているようです。

UIButton のイベントを拾う

Hello, world の例では、このライブラリを使う利点が見いだせそうにありません。 そこで、ネイティブアプリにありがちな UI イベントの処理に利用する例を見てみることにします。 とりあえず、細かな話は抜きにして、適当なビューに UIButton を作成して ViewController の viewWillAppear に次のようなコードを記述してください。

import UIKit import RxSwift class ViewController: UIViewController { private var disposeBag = DisposeBag @IBOutlet weak var button: UIButton! override func viewWillAppear(animeted: Bool) { super.viewWillAppear(animated) button.rx_tap .subscribeNext { NSLog("チマメ隊") } .addDisposableTo(disposeBag) } override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) disposeBag = DisposeBag() } }

これを実行して、ボタンをタップすると 「チマメ隊」というログ出力がされるはずです。 ボタンのタップイベントの処理方法は多々ありますが、Rx を使ったパターンはわりあい見通しの良いものになっているかと思います。

UITextFiled のイベントを拾う

続いてテキストフィールドの文字入力イベントを拾う例を考えてみましょう。 適当なビューに UITextField を作成して、 viewWillAppear に以下のようなコードを記述してみてください。

textField.rx_text .map { "はぁ... " + $0 + "さん..." } .subscribeNext { NSLog($0) } .addDisposableTo(disposeBag)

すると以下のように、文字入力や削除のたびにイベントが発生して、NSLog でログが出力されるのがわかると思います。

もしテキストフィールドに特定の値が来たときだけのイベントを拾いたい際は、例えば以下のように filter をしてあげれば良い感じになります。

textField.rx_text .filter { $0 == "ココア" } //=> テキストフィールドに「ココア」と入力されたときだけ以後のステップに進む .map { "はぁ... " + $0 + "さん..." } .subscribeNext { NSLog($0) } .addDisposableTo(disposeBag)

UISlider の値に応じて UILabel のフォントサイズを変更する

さて、もっとインタラクティブな例を見てみましょう。スライダーの値に応じてラベルの文字サイズを変更するような処理を書きたいとします。このとき、以下のように UISlider の値の変更イベントを UILabelrx_attributedTextbind してあげれば OK です。

slider.rx_value .map { NSAttributedString( string: "特殊相対性理論", attributes: [NSFontAttributeName: UIFont(name: "HiraginoSans-W3", size: CGFloat($0))!] ) } .bindTo(label.rx_attributedText) .addDisposableTo(disposeBag)

UISlider の上限値と下限値をうまい感じに設定すれば、以下のようにスライダーの値に応じてラベルのサイズが変化するような実装が簡単に行えます。

RxSwift を使ってみよう

以上の例を見ていただければ、だいたいの UI コンポーネントのイベントは RxSwift(RxCocoa) を使って処理できることが分かっていただけたのではないでしょうか。

iOS 開発での UI イベント処理は、コールバックやデリゲートで対応していくパターンが多いと思いますが、RxSwift を使うと宣言的に書くことができるようになっています。特に、コールバックを避けることによりイベント処理を各 View のクラスに散らさずにすむことにより、コードの見通しが格段に向上するのではないかと考えています。

また、map, filter をはじめてとして様々な高階関数を利用できる点も優れています。これらをチェインでつなぐことにより、イベントデータの変形や抽出などが自由自在にできます。

以上の内容は「Reactive Programming とは何か」などということは知っていなくても、理解できたのではないでしょうか? RxSwiftを使おうか迷っている方は、巷に出回っている難しい記事をみて頭をひねるより、まず使ってみることをお勧めします。

ここでご紹介した RxSwift の利用例は、GitHub にて公開していますので是非 clone して動かしてみてください。Xcode7.2 で動作を確認しています。

$ git clone git@github.com:53ningen/rxswift-examples.git $ cd ./rxswift-examples $ bundle install $ bundle exec pod install $ open ./rxswift-examples.xcoworkspace

第 2 羽 通信をするお話

第 1 羽でみてきたように RxSwift を利用すると、UI イベントを手軽にハンドリングすることができます。 アプリ開発で行う定番の処理としてもう一つ押さえておかなければならないのは、通信処理かと思います。 実はこれも RxSwift を用いて良い感じに扱うことができます。

利用方法も非常に簡単なのでまずはコード例をみてみましょう。NSURLSession のインスタンスに rx_response というプロパティが生えているので、これを使えば以下のように簡単に通信処理が行えます。

import RxSwift import UIKit class SimpleNetworkingSampleViewController: UIViewController { private var disposeBag: DisposeBag = DisposeBag() private let backgroundWorkScheduler = ConcurrentDispatchQueueScheduler(globalConcurrentQueuePriority: .High) override func viewWillAppear(animated: Bool) { super.viewWillAppear(animated) let url = NSURL(string: "http://gochiusa.com")! let request = NSURLRequest(URL: url) NSURLSession.sharedSession().rx_response(request) .subscribeOn(backgroundWorkScheduler) .observeOn(MainScheduler.sharedInstance) .subscribeNext { (data, response) -> Void in if let str = String(data: data, encoding: NSUTF8StringEncoding) { print(str) } } .addDisposableTo(disposeBag) } override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) disposeBag = DisposeBag() } }

subscribeNext で渡しているクロージャの中身を見れば大体察しがつくかと思いますが、rx_response(NSData, NSHTTPURLResponse) を返してくれるものになってます。

RxSwift を用いた通信エラー処理

さて、上記の例に加えてエラー処理をしたいケースを考えてみましょう。たとえば、存在しないページにアクセスした場合は、404 のステータスコードとページが見つからない旨のレスポンスが返ってくると思います。

let url = NSURL(string: "http://gochiusa.com/hogehoge")! let request = NSURLRequest(URL: url) NSURLSession.sharedSession().rx_response(request) .subscribeOn(backgroundWorkScheduler) .observeOn(MainScheduler.sharedInstance) .subscribeNext { (data, response) -> Void in if let str = String(data: data, encoding: NSUTF8StringEncoding) { print("status code: " + String(response.statusCode)) print("response body: " + str) } } .addDisposableTo(disposeBag) // => ログ出力 // status code: 404 // response body: <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN"> // <html><head> // <title>404 Not Found</title> // </head><body> // <h1>Not Found</h1> // <p>The requested URL /hoge was not found on this server.</p> // </body></html>

Rx にはエラー的な状況をハンドリングする仕組みがあります。例えば、ステータスコードが 200 のときには、レスポンスを String で、それ以外のときには NSError を流すには次のように書けば OK です。

NSURLSession.sharedSession().rx_response(request) .flatMap({ (data, response) -> Observable<String> in if let string = String(data: data, encoding: NSUTF8StringEncoding) where response.statusCode == 200 { // 成功値を流すには flatMap の中で just(値) を返せばよい return just(string) } else { // エラーを流すには flatMap の中で failWith(NSError) を返せばよい return failWith(NSError(domain: "ConnectionError", code: response.statusCode, userInfo: nil)) } }) .subscribeOn(backgroundWorkScheduler) .observeOn(MainScheduler.sharedInstance) .subscribe({ (event) -> Void in switch event { case .Next(let string): print(string) // 値の処理 case .Error(let e): self.showDialog(e) // エラー処理 case .Completed: break } }) .addDisposableTo(disposeBag)

コードからわかるように、flatMap の中で just(値) を返すと成功値を流すことができます。 また failWith(ErrorType) を返すとエラーを伝播させることができます。

通信処理の階層化

簡単なアプリであれば、上記の NSURLSession.rx_responseViewController から直接呼び出してしまっても良いのですが、ちょっと複雑な画面やロジックが入るようなアプリを作る際にこれをやってしまうと、たちまちどこに何が書いてあるのかが分からなくなります。

そこで、通信処理とそのレスポンスの加工を、以下の 3 つの要素に分離して実装を行うパターンが個人的には気に入っています。

  1. HTTP リクエストを組みたて通信を行う責務を持つ HttpClient
  2. 利用するウェブ API に与えるパラメータとそのレスポンスを抽象化した ApiClient
  3. ウェブ API からのレスポンスやキャッシュなどからデータを取得し、クライアント側からコレクションのように操作を加えられるようにした Repository

こうすることにより、例えばリクエストの組み立てなどの部分で、同じロジックを繰り返し書くことを防げます。またスタブを利用したテストを書けば、各処理部単位で正しい実装が行われていることを保証することもできます。また、アプリ開発においてはデザイン確認用のレスポンスを返すスタブを作ると便利かもしれません。

次羽からは HttpClientApiClient の実装についてみていきます。

第 3 羽 Call Me HttpClient.

HTTP リクエストを組み立てる機能単位を HttpClient にまとめることを考えます。 こうすることによりクエリパラメータの URL エンコード処理やリクエストヘッダーを作る部分の共通処理をまとめることができます。 また、HttpClient としてプロトコル(他言語でいうところのインターフェース)を切っておくことにより、 実際の通信時に用いるライブラリや Swift の API への依存を外側に晒さずにすむという効果もあります。 作るものの全体像は下図のようなものになります。

RequestParameterRequestHeader は、単純に Key-Value 的なただのデータ構造です。 実装的には以下のように typealias としてあげるのが一番簡単ですが、個別に struct を定義しても大丈夫です。 RxHttpClient のプロトコル自体もそのまま書き下せば良いかと思います。

import RxSwift import Foundation public typealias RequestParameter = (key: String, value: String) public typealias RequestHeader = (key: String, value: String) public protocol RxHttpClient { func get(url: NSURL, parameters: [RequestParameter], headers: [RequestHeader]) -> Observable<(NSData, NSHTTPURLResponse)> func post(url: NSURL, parameters: [RequestParameter], headers: [RequestHeader]) -> Observable<(NSData, NSHTTPURLResponse)> func put(url: NSURL, parameters: [RequestParameter], headers: [RequestHeader]) -> Observable<(NSData, NSHTTPURLResponse)> func delete(url: NSURL, parameters: [RequestParameter], headers: [RequestHeader]) -> Observable<(NSData, NSHTTPURLResponse)> }

ここで定義したプロトコルの実装クラスは、ちゃんと動けばどんな感じにしても良いかと思いますが、Alamofire などライブラリに依存しない形で実装した一例を、GitHub: https://github.com/53ningen/RxHttp に公開していますのでそちらをご覧いただければ幸いです。

通信関連のエラー処理はこのあたりの層でハンドリングしてあげると以後の層の実装がすっきりすると思いますので、以前に紹介している flatMap 関数などを使い、よしなにエラー処理を記述すると使い勝手がなかなか良い HttpClient になるのではないでしょうか?

第 4 羽 API を探す日常

続いて、アプリで用いるウェブ API の操作を抽象化した ApiClient について考えてみましょう。 使うエンドポイントに対して Controller で個別にリクエストパラメータやヘッダを指定するのは煩わしいですし、コード自体の見通しも悪くなります。 今回は例として、 Qiita API: https://qiita.com/api/v2/docs の記事リストを取得する API: GET /api/v2/items を扱ってみます。 実装する ApiClient の構造は以下の図のような形となります。

QiitaApiClient の持つメソッドを見ると、指定すべきパラメータがはっきりと理解できるかと思います。 こうしてあげることによって、クライアントコードで API のデータを取得する際に迷わずパラメータを指定することができます。 図中に出てくる登場人物を確認しましょう。 まず、HttpClient は第 3 羽で作成したものと同じものとなります。 今回作成したい ApiClient は、メソッドの引数をいい感じに加工して RequestParameterRequestHeader を生成し、 HttpClient に与えることにより、API への通信を行うという仕組みになっています。 ItemRecord は Qiita の投稿記事(item)レスポンスのデータ構造を定義したクラスです。 Qiita API は、JSON 形式でレスポンスを返してくれますが、Controller などで JSON をそのまま扱うのは見通しが悪いので、 こういったクラスを作成して、そのインスタンスにマッピングしてあげるのが良いかと思います。 プロトコルとデータ構造のコードはだいたい以下のようになります。

public protocol QiitaApiClient { func getItems(offset: Int, limit: Int) -> Observable<[ItemRecord]> func getItems(keyword: String, offset: Int, limit: Int) -> Observable<[ItemRecord]> } public struct ItemRecord { let id: String let title: String let url: String // 以下同様に... }

ここで定義したプロトコルの実装クラスでは、JSON を ItemRecord にマッピングするなど細かい作業が多くなってきます。RxSwift を用いた ApiClient の作り方とはまた離れた話になってきますので、GitHub: https://github.com/53ningen/rxswift-examples/tree/master/rxswift-examples/Infrastructure に上げてあります。興味がある方は、そちらをご覧ください。

第 5 羽 君のためなら開発する

RxSwift を用いた UI イベント処理と通信処理について簡単な解説を行ってきましたが、いかがでしたでしょうか。 Rx を利用することによりコールバックをほとんど使わずにイベント処理が行えたり、伝播させるデータの変形やフィルタが手軽に行えることがわかっていただけたのではないでしょうか。 また例外を使わずにエラー処理を行うことができる点も魅力的だと思います。今回の記事でご紹介した内容は Rx の一側面にすぎませんので、ライブラリを使いながら様々な API を触って、活用法を考えていくと面白いのではないかと思います。

Copyright © 53ningen.com