30
29

More than 3 years have passed since last update.

ReactiveCocoa 4.0入門 (Swift)

Last updated at Posted at 2015-12-21

この記事はACCESS Advent Calendarの21日目の記事です。

自分は業務ではもっぱらJavaScript developer ですが、
今日は趣味で開発しているiOSアプリを作る中で利用しているReactiveCocoaについての知見を共有したいと思います。
今回は、「Observerパターン」「非同期API通信の際にPromise的に使う」について述べたいと思います。

なぜ、ReactiveCocoaか?

NSNotificationCenter, KVO への不満

swift でアプリを書き始めようと思った時に既存のcocoaのAPIがswiftと相性が良くないなと思ったのがきっかけになります。
具体的には、NSNotificationCenter, KVOです。

最初swiftを勉強がてらアプリを書き始めた当初、例によって、ObserverパターンのためにNSNotification Center, KVOを利用して言いました。

func addObserver() {
    let center = NSNotificationCenter.defaultCenter()
    center.addObserver(self, selector:"somethingChanged:", name:"notification_name", object: self) 
}


func somethingChanged(notification: NSNotification) {
    if let info = notification.userInfo {
        ....
    }
}


userInfoはAnyObjectなので、キャストして使う必要があります。swiftで型チェックが効いて安全なコードが書けると思っていたのに、これでは残念です。
最初は自分で個々のモデルごとに簡易なObserverパターンを実装していたのですが、誰か汎用的なものを作っているだろうと探してたどり着いたのが、ReactiveCocoaでした。
なので、私はFRPについては詳しくなく型チェックがいい感じに効くObserverableなものとしてReactiveCocoaを使っています。

準備

Carthageを通しての利用

ReactiveCocoaはCocoaPods, Carthageでの利用が可能です。自分はビルド時間が短縮されるので、Carthageを使っています。
brewなどで[Cartahge]インストールをしましょう。

brew install carthage

また、現時点での最新版 4.0alpha4を使うこととします。
ちなみにですが、Carthage 自体もReactiveCocoaを使って描かれているみたいです。

github "ReactiveCocoa/ReactiveCocoa" "v4.0.0-RC.1"
github "Alamofire/Alamofire"
github "SwiftJSON"
carthage bootstrap --no-use-binaries --platform OSX

今回は、JSON APIのクライアントを作ってみたいと思うので、他のライブラリも一緒に入れています。

Playgroundでの使用のためのxcodeworkspaceの作成

調べた感じだとPlaygroundからdynamic Frameworkとしてライブラリを使うためには、同じワークスペース内にライブラリのプロジェクトがある必要がありそうなので、以下のようにプロジェクトを作成しました。

  1. Xcode -> File -> New -> Cocoa Application
    • ReactiveCocoaSampleとして作成
  2. Xcode -> File -> Save as workspace
  3. Add Files to "ReactiveCocoaSample"でCarthage/Checkouts以下のライブラリのxcodeproj をworkspaceに追加
  4. Xcode -> File -> New -> Playground でPlaygourndの作成

使ってみよう

ケース1: Observerパターンの実装 (NotificationCenter, KVOの代替)

var pipe = Signal<ItemEvent, NSError>.pipe()

var itemSignal:   Signal<ItemEvent, NSError>          = pipe.0
var itemObserver: Signal<ItemEvent, NSError>.Observer = pipe.1

var items: [String] = []

itemSignal.observeNext { event in
    switch event {
    case .ItemAdded(let entry):
        print("entry added")
        items.append(entry)
    case .ItemRemoved(let entry):
        print("entry removed")
        items = items.filter { $0 != entry }
    }
}

var item1 = "item1"
var item2 = "item2"
var item3 = "item3"
itemObserver.sendNext(ItemEvent.ItemAdded(item1))
itemObserver.sendNext(ItemEvent.ItemAdded(item2))
itemObserver.sendNext(ItemEvent.ItemAdded(item3))

print("entry count: \(items.count)")
itemObserver.sendNext(ItemEvent.ItemRemoved(item1))
print("entry count: \(items.count)")

  • Signal<Value, Error: ErrorType>を使います。 Signal は成功した場合の値と失敗した場合の型引数をとして指定できます。今回は、記事に対するイベントをEntryEventとして定義したいと思います。swiftのenumとして定義できます。文字列として定義しなくて良いのが型チェックが効くのでいい感じですね。
  • Signal#observeNextでイベントが発生した時の処理を登録します。(addObserver) ここでもswiftのクロージャを登録できるのでいい感じです。ただ、メモリークには気をつけましょう。
  • Signal.Observer#sendNext でイベントが発生させます。(postNotification)

ケース2: 非同期API通信をPromiseっぽく書く

シンプルな例


func fetchLgtm() -> SignalProducer<String, NSError> {
    return SignalProducer<String, NSError> { (observer, disposable) in
        let url = "http://www.lgtm.in/g"
        let request = Manager.sharedInstance.request(.GET, url, headers: ["Accept":"application/json"])
        request.responseJSON(options: NSJSONReadingOptions()) { response in
            if let e = response.result.error {
                observer.sendFailed(e as NSError)
            } else {
                observer.sendNext(JSON(response.result.value!)["markdown"].stringValue)
                observer.sendCompleted()
            }
        }
        disposable.addDisposable({ request.cancel() })
    }

}

var signalProducer = fetchLgtm()

signalProducer.on(started: {
    }, failed: { error in
        print("failed \(error)")
    }, completed: {
        print("completed")
    }, interrupted: {
    }, terminated: {
    }, disposed: {
    }, next: { value in
        print("value \(value)")
    }).startOn(QueueScheduler()).start()

こちらはSignalProducer<Value, Error : ErrorType>を使います。SignalProducerのコンストラクタで通信処理を書き、通信処理が完了したコールバックの中で、observerに対してsendNext, sendCompletedしています。jQuery.deferredで通信処理が終わった後にresolveするのと同じような感じです。
また、キャンセルするための処理がaddDisposableとして追加します。これはjQuery.derredにはないですが、細やかな通信制御が必要なmobileアプリには必須な機能で非常に嬉しいです。

応用例:複数のリクエストを束ねる

応用例として、複数のリクエストを送ってすべて終わったら何かをするというような処理を書いてみたいと思います。jQuery.whenのようなものですね。

var signals: [SignalProducer<String, NSError>] = []

for i in 0..<3  {
    signals.append(fetchLgtm())
}

var multiSignal: SignalProducer<String, NSError> = signals.reduce(SignalProducer.empty, combine: { (currentSignal, nextSignal) in
    currentSignal.concat(nextSignal)
})


multiSignal.on(started: {
    }, failed: { error in
        print("failed \(error)")
    }, completed: {
        print("completed")
    }, interrupted: {
    }, terminated: {
    }, disposed: {
    }, next: { value in
        print("value \(value)")
}).startOn(QueueScheduler()).start()

少しトリッキーな書き方ですが、Signal#concatで2つのSignalを1つにまとめることができるので、それを先頭の要素から順に呼び出して1つのSignalにしています。今回は直列につないでいます。

まとめ

今回はReactiveCocoaの使い方の一例として

  1. Observerパターン
  2. 非同期API通信の際にPromise的に使う

を紹介しました。
ReactiveCocoa自体はもっと多機能で便利な使い方ができますが、まずは、こういった基本的な部分で利用すると良いのではないかと思います。コードは こちら においておきます。


この記事はACCESS Advent Calendar 21日目の記事です
明日は @sassy_watson さんが「開発を円滑にするためのコミュニケーションを考える」話をしてくれます。

30
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
29