LoginSignup
32
34

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-01-07

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

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

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

SRGModelEvent

使い方

導入

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

Podfile
pod 'SRGModelEvent'

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

#import "SRGModelEvent.h"

シンプルな利用例

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

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()

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

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

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

// 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では上の例のObjective-Cの物を載せています。

より具体的な利用例

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

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

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

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

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

Article.swift
// Articleは1つの記事を表すデータモデルクラス
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で投稿イベントの監視&通知を実装する例は以下のようになります。

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から利用する時は以下のように利用します。

作った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"という文字列を直接渡しています。

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

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

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

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

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

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

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

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

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

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する方法は以下のようになります。

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

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

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

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

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

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

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

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

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

KVOを使った方がよい場面

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

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

  • 先ほどのArticleArticleViewに描画している場合
    • →ArticleのプロパティとViewの要素が明確に紐付いているのでKVOが適している
  • UITableViewControllerCellに描画していて、一覧のデータは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の紹介と、他の監視方法との簡単な比較でした。

32
34
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
32
34