はじめに
Swift で iOS アプリを実装し直すにあたって、 ただ Swift に翻訳するだけではつまらないので、 MVVM アーキテクチャで実装することに決めた。
View/ViewController と ViewModel のバインド、 イベントや非同期 API 呼び出しのインタフェースを統一するために、 ReactiveCocoa を導入する。
CocoaPods で RactiveCocoa をインストール
Podfile に
pod "ReactiveCocoa"
を追加して pod install
。
Bridging-Header で ReactiveCocoa のヘッダーファイルをインポート
<プロジェクト名>-Bridging-Header.h に次の行を追加。
#import <ReactiveCocoa/ReactiveCocoa.h>
これで Swift から ReactiveCocoa が使えるようになった。
RACSignal を作成してみる
API 非同期呼び出しのインタフェースを RACSignal で統一したいので、RACSignal でラップする。
class CommentViewModel: NSObject { var ownerName: String! var repoName: String! var issueId: Int! var comment: String? func postComment() -> RACSignal { return RACSignal.createSignal({ (subscriber) -> RACDisposable! in let manager = AFHTTPRequestOperationManager.manager() let operation = manager.POST( "/repositories/¥(self.ownerName)/¥(self.repoName)/issues/¥(self.issueId)/comments", parameters: ["comment": self.comment], success: { (operation, response) -> Void in subscriber.sendNext(response) subscriber.sendCompleted() }, failure: { (operation, error) -> Void in subscriber.sendError(error) } ) return RACDisposable(block: { () -> Void in operation.cancel() }) }) } }
UITextView に ViewModel をバインドしてみる
UITextView の text に変更があったら即座に ViewModel に反映させてみる。 Objective-C では RAC マクロを使ってバインドできるけど、Swift では RAC マクロが使えないので、 低レイヤーのメソッドを使うしかなかった。
self.commentTextView.rac_textSignal().setKeyPath("comment", onObject: self.viewModel)
ちなみに、同じことが UITextField でも可能。
ViewModel のプロパティを UIBarButtonItem にバインドしてみる
例えば、「ViewModel の comment プロパティが空ではないとき UIBarButtonItem を有効にする」 というのを ReactiveCocoa で実装するとこんな感じ。
self.viewModel.rac_valuesForKeyPath( "comment", observer: self.viewModel ).map({ (next) -> AnyObject! in let newComment = next as String return !newComment.isEmpty }).setKeyPath("enabled", onObject: self.postButtonItem)
RACCommand を使って ViewModel のメソッドも UIBarButtonItem にバインドしてみる
「ViewModel の comment プロパティが空ではないとき UIBarButtonItem を有効にする」だけでなく、 UIBarButtonItem をタップしたときのアクションも併せて ReactiveCocoa で実装。
let commentSignal = self.viewModel.rac_valuesForKeyPath( "comment", observer: self.viewModel ).map({ (next) -> AnyObject! in let newComment = next as String return !newComment.isEmpty }) let postCommand = RACCommand( enabled: commentSignal, signalBlock: { (input) -> RACSignal! in return self.viewModel.postComment() } ) self.postButtonItem.rac_command = postCommand
ReactiveCocoa を使ってみて
インタフェースを RACSignal に統一できるので、コードを書きやすい。 特に RACSignal 化した非同期呼び出しは、引数でコールバックを渡す必要がないのでスッキリする。
ただ、コントロールにバインドする RACSignal や RACCommand は、 ViewModel のプロパティとして実装したほうが良かった。
ReactiveCocoa 難しい。 RACSignal にようやく慣れてきたけど、 RACCommand やその他の機能はまだ理解が追いついていない。