[iOS] シンプルで安全な方法でModelのイベントをObserveする

GUIアプリを`MVC`で作っている場合、`Model`の特定のイベントに基づいて処理を行いたいという場面はよくあると思います。`VC`での`Model`の`Observe`、もしくは`Model`の中でもレイヤーを分ける際などです。

`Cocoa Framework`には`KVO(キー値監視)`という、Objectの値の変更を監視するための仕組みがあります。ただ`property`に紐付かない抽象的なイベントを監視したい際など`KVO`が上手くはまらないケースも多々あります。
(この記事の一番下に具体的な`KVO`等の各監視方法の使い分けについて詳細を書いています)

そういう場合は`NSNotificationCenter`の利用を検討すると思いますが、少し癖のあるAPIなので、お手軽にModel層のイベントを監視するためのラッパーライブラリとして`SRGModelEvent`を作ってみました。

## SRGModelEvent

https://github.com/soragoto/SRGModelEvent

## 使い方

### 導入
Podfileに以下の行を追加して`pod update`します。

```ruby:Podfile
pod 'SRGModelEvent'
```

`Objective-C`の場合利用したいクラスで、`Swift`の場合は`Bridging-Header`ファイルで以下のファイルをimportしてください

```objc
#import "SRGModelEvent.h"
````

### シンプルな利用例

以下のように、`observer/notify`処理を書くことができます。

~~~swift:Swift
// 利用者(User)に関するイベントを管理するためのインスタンスを生成
let userEvent = SRGModelEvent(key: "user")

// イベント名を文字列で決めてobserve/notifyに使います。例えば"login"の場合
let loginObserver = userEvent.observe("login", handler: { data in
    // ここにユーザのサービスログインに紐付いて行いたい処理を書く
})

// ログイン完了時にobserverに通知を行う
userEvent.notify("login")

// observerは以下のように解除可能
loginObserver.stopObserving()
~~~~

`KVO`や`NSNotificationCenter`をそのまま利用する場合に較べて、比較的読みやすいコードで`Model`の`observe`ができると思います。

### observe/notify間でデータのやりとり

また`observe/notify`間でデータのやりとりを行うこともできます。例えば先ほどの"login"で、ログイン日時を渡すとすると以下のようなコードになります。

```swift
// notifyするときに任意のデータを渡すことができます
userEvent.notify("login", data:[
    "login_date" : NSDate()
])

// observerはhandlerの中でdataを受け取ることができます
let loginObserver = userEvent.observe("login", handler: { data in
    let loginDate = data["login_date"] as NSDate;
    // ログイン時にloginDateを利用してやりたい処理
})
       
```

### Objective-Cの場合の例

`Objective-C`でも、もちろん利用することができます！ [githubのREADME](https://github.com/soragoto/SRGModelEvent)では上の例の`Objective-C`の物を載せています。

## より具体的な利用例

もう少し具体的な例を考えてみましょう。
何かのブログサービスのクライアントアプリを想定してみます。`Rest API`等を通して記事を取得したり投稿したりするアプリです。

そのアプリで利用者が<b>アプリから記事を投稿した際のイベント監視<b>の実装方法を考えてみます。

アプリ内の`Model`を考えます。１つの記事のデータを表すクラスを`Article`、記事の操作（取得、編集、削除）などを操作をクラスの`ArticleManager`の２つのクラスがあるとします。

### 記事のデータを表すモデル

１つの記事を表す`Article`クラスは以下のような感じになると思います

```swift:Article.swift
// Articleは１つの記事を表すデータモデルクラス
class Article: NSObject {
    let id: String
    let title: String
    let body: String
    init(plain:Dictionary<NSString,Any>) {
        /* Rest APIの結果から自分を初期化 */
        self.id = plain["id"] as String
        self.title = plain["title"] as String
        self.body =  plain["body"] as String
    }
}
````

### 記事の操作をするモデルが投稿完了後にobserverへ通知

記事の取得や投稿をなどの操作をクラスである`ArticleManager`で投稿イベントの監視＆通知を実装する例は以下のようになります。

```swift:ArticleManager.swift
// ArticleManagerは記事の投稿,取得,削除などの記事への操作を行うモデルクラス
class ArticleManager: NSObject {
    
    /* Articleへのイベントを管理する */
    let event:SRGModelEvent
    
    /* シングルトンで取得できるようにしておく */
    class var sharedManager : ArticleManager {
        struct Static {
            static let instance : ArticleManager = ArticleManager()
        }
        return Static.instance
    }

    /* init時にself.eventもインスタンスを生成しておく */  
    override init() {
        self.event = SRGModelEvent(key: "article")
    }
    
    /* 記事の投稿を行うメソッド */
    func post(title:NSString, body:NSString){

        /* 本来RestAPI等を叩いて投稿する処理がここにあるはず */
        /* (&本来は非同期なので完了時のcallbackなども引数に渡すべきだが話を簡単にするために省略) */

        /* 以下通信に成功して投稿結果がdummyAPIResponseに入ってるとする */
        let dummyAPIResponse = [ "id": "dummy", "title" : title, "body" : body]
        /* 通信結果からArticleインスタンス作成 */
        let article = Article(plain:dummyAPIResponse)

        /* observerに投稿完了を通知！ */
        self.event.notify("create",data:[
            "article" : article
        ])
    
    }
    
}
```

上のコードでは`post`の中で投稿完了後に`self.event.notify`に"create"イベントがあったことを通知しています。また作成された記事のarticleインスタンスも一緒に送っています。

実際にこれを`Controller`や他`Model`から利用する時は以下のように利用します。

```swift:作ったModelでの利用方法
/* とあるクラスでの監視処理  */
let articleManager = ArticleManager.sharedManager
articleManager.event.observe("create",handler:data in
     let article = data["article"] as Article
     // 新記事articleの投稿が完了した時に実行したい処理をここに書く
});

/* 別のクラスで投稿処理 */
let articleManager = ArticleManager.sharedManager
articleManager.post(title:"タイトル",body:"ほげ")
````

さて、これで動作は問題ないはずです。しかし上のコードの`observe`する部分にはいくつか問題があります。

### 上のコードのobserve部分の良くない点

まず第一に、observeの引数に`"create"`という文字列を直接渡しています。

```swift:問題点/イベント名を文字列で指定している
articleManager.event.observe("create",handler:data in
````

`Model`の`observe`は基本的に`1対多`の関係になります。なので、このように文字列で指定してしまうとイベント名が変わったときに手動で多くの箇所を修正する必要があります。またコードを書く時にXCodeの静的解析による自動置換も使えません。

`observe`に渡している`handler`の中の下記の部分も同様です。

```swift:問題点/型をAny型からキャストしている
let article = data["article"] as Article
````

こちらはdataの中身のkeyを文字列でしていて、さらにそれが`Article`であるという前提で処理を行っています。なので、このdata構造に仕様変更があったとき、手動で直さなければいけません。 また上でも書いたように`Observer`パターンは`1対多`になる前提のため、多くの箇所の修正が必要になり、対応漏れなどが発生する可能性があります。

そして`Any`型や文字列でやりとりをしているため、漏れがあったとしても静的解析で警告は出ないので実行するまで問題に気づくことができません。

### 利用側がより安全＆簡単に使える設計にする

上のような問題点を改善するために`ArticleManager`の`event`は`private`にして非公開にします。そして投稿を`observe`するための専用メソッドを作ってみましょう。

具体的には以下のようになります。

```swift:ArticleManager.swift
class ArticleManager: NSObject {
    
    /* [変更]eventはprivateにして非公開にする */
    private let event:SRGModelEvent
    
    class var sharedManager : ArticleManager { /* 省略 */  }
    
    override init() {
        self.event = SRGModelEvent(key: "article")
    }
    
    func post(title:NSString, body:NSString){
        /* 省略 */
        let dummyAPIResponse = [ "id": "dummy", "title" : title, "body" : body]
        let article = Article(plain:dummyAPIResponse)
        self.event.notify("create",data:[
            "article" : article
        ])
    }
    
    /* [追加]投稿完了をobserveするためのメソッドを用意 */
    func observePostEvent(onPostHandler:(article:Article) -> Void ){
        self.event.observe("create", handler:{ data in
            onPostHandler( article: data["article"] as Article )
        })
    }
    
   
}
```

上のように`observePostEvent`を定義することで外側からobserveする方法は以下のようになります。

### 改善したモデルの利用イメージ

```swift:モデルの利用
let articleManager = ArticleManager.sharedManager
articleManager.observePostEvent({ (article:Article ) in
    // 新記事articleの投稿が完了した時に実行したい処理をここに書く
})
````

`closure`は引数として`Article型`の`article`インスタンスを受け取ることが明示されています。このため補完やエラー表示などの`IDE`の機能を使って素早く利用したり問題を検知することができます。

そして文字列でのイベント名定義`"create"`や`Any型`からのキャストの、緩い部分は全て`ArticleManager`の中に閉じることができました。 `observe/notify`の構造を変えたとしても、変更は全て`ArticleManager`の中だけに閉じ外に影響することがありません。

このような書き方にすると`ArticleManager`に`observePostEvent`を定義する手間は増えます。しかし「observeされる処理」を書くのは１度だけですが、「observeする側の処理」は複数箇所書くはずです。

なので、される側に１回手間をかけるだけで、で何回も書く「observeする処理」が簡単に安全に書けるようになるなら払う価値のあるケースのほうが多いでしょう。


## Model監視の際の「didSet/willSet」や「KVO」との使い分け

### didSetやwillSetを使ったほうがよい場面

`swift`にはプロパティの値に変化があったときの処理を登録しておける`didSet/willSet`という機能があります。
`observe対象`と`observeする側`が同一クラス内の場合は、まずはこの`didSet/willSet`を検討してみるのが良いでしょう。

### KVOを使った方がよい場面

`Cocoa Framework`には`property`を監視するための`KVO`という仕組みがあります。
observe対象が`property`に明確に紐づくデータの場合は、こちらの検討を利用してみると良いと思います。

特に`MVC`で`VC->M間`のバインディングには`KVO`が適していることが多いでしょう。
例えば具体的な例をあげると....

* 先ほどの`Article`を`ArticleView`に描画している場合
 * →ArticleのプロパティとViewの要素が明確に紐付いているので`KVO`が適している
* `UITableViewController`で`Cell`に描画していて、一覧のデータは`ArticleManager`のプロパティ`articles`を表示しているような場合
 * →プロパティと表示しているデータが明確に紐付いているので`KVO`が適している

### SRGModelEventを使ったほうがよい場面

`property`に紐付かないようなModelのイベントで監視処理を行いたい際、`SRGModelEvent`を使ったほうがよいでしょう。
例えば`Article`の「編集(edit)」というイベントはプロパティである`Article.title`だけや`Article.body`だけを監視する方法はマッチしていません。

具体的な利用シーンとしては

* アプリにチュートリアル機能があり`Tutorial`のモデルがある。しかし`Article`などのメインロジックの`Model`内には`Tutorial`関係のコードを入れたくないようなシーン
 * →`Tutorial`のモデルが他のModelを`observe`してチュートリアルロジックを組んでいく実装になるので`SRGModelEvent`のような方法でModelをobservableにするのが適しています
* 記事一覧を取得して`TableView`に描画するとき、そことは別の`View`パーツに新着未読件数を表示するパーツがあり、同時に更新したい
 * Modelへのメソッドで副次的に複数箇所を更新する必要があり、それらが`property`と紐付いていないような場面であれば`KVO`で書くのはつらいので`SRGModelEvent`のほうが適していると思います

## まとめ
以上、モデルのイベント監視のためのライブラリ`SRGModelEvent`の紹介と、他の監視方法との簡単な比較でした。