読者です 読者をやめる 読者になる 読者になる

Swift で ReactiveCocoa を使う

ios swift

はじめに

SwiftiOS アプリを実装し直すにあたって、 ただ 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 やその他の機能はまだ理解が追いついていない。