GUIアプリをMVC
で作っている場合、Model
の特定のイベントに基づいて処理を行いたいという場面はよくあると思います。VC
でのModel
のObserve
、もしくはModel
の中でもレイヤーを分ける際などです。
Cocoa Framework
にはKVO(キー値監視)
という、Objectの値の変更を監視するための仕組みがあります。ただproperty
に紐付かない抽象的なイベントを監視したい際などKVO
が上手くはまらないケースも多々あります。
(この記事の一番下に具体的なKVO
等の各監視方法の使い分けについて詳細を書いています)
そういう場合はNSNotificationCenter
の利用を検討すると思いますが、少し癖のあるAPIなので、お手軽にModel層のイベントを監視するためのラッパーライブラリとしてSRGModelEvent
を作ってみました。
SRGModelEvent
使い方
導入
Podfileに以下の行を追加してpod update
します。
pod 'SRGModelEvent'
Objective-C
の場合利用したいクラスで、Swift
の場合はBridging-Header
ファイルで以下のファイルをimportしてください
# import "SRGModelEvent.h"
シンプルな利用例
以下のように、observer/notify
処理を書くことができます。
// 利用者(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"で、ログイン日時を渡すとすると以下のようなコードになります。
// 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は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は記事の投稿,取得,削除などの記事への操作を行うモデルクラス
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
から利用する時は以下のように利用します。
/* とあるクラスでの監視処理 */
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
Model
のobserve
は基本的に1対多
の関係になります。なので、このように文字列で指定してしまうとイベント名が変わったときに手動で多くの箇所を修正する必要があります。またコードを書く時にXCodeの静的解析による自動置換も使えません。
observe
に渡しているhandler
の中の下記の部分も同様です。
let article = data["article"] as Article
こちらはdataの中身のkeyを文字列でしていて、さらにそれがArticle
であるという前提で処理を行っています。なので、このdata構造に仕様変更があったとき、手動で直さなければいけません。 また上でも書いたようにObserver
パターンは1対多
になる前提のため、多くの箇所の修正が必要になり、対応漏れなどが発生する可能性があります。
そしてAny
型や文字列でやりとりをしているため、漏れがあったとしても静的解析で警告は出ないので実行するまで問題に気づくことができません。
利用側がより安全&簡単に使える設計にする
上のような問題点を改善するためにArticleManager
のevent
はprivate
にして非公開にします。そして投稿をobserve
するための専用メソッドを作ってみましょう。
具体的には以下のようになります。
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
の中だけに閉じ外に影響することがありません。
このような書き方にするとArticleManager
にobservePostEvent
を定義する手間は増えます。しかし「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
に明確に紐づくデータの場合は、こちらの検討を利用してみると良いと思います。
特に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
の紹介と、他の監視方法との簡単な比較でした。