[iOS] EmitterKitでイベントとかプロパティ監視する

2014/10/2

こんにちは、きんくまです。

イベント管理のライブラリを探していたら、こんなページが見つかりました。
>> Swift Toolbox

その中にEmitterKitというライブラリがありまして、見てみたら使いやすそうでした。
>> aleclarson/emitter-kit

試してみたので、メモです。
インストール方法はgithubのページを参照してください。

簡単な使い方 Signal

イベントには2種類あり、SignalとEventです。
Signalはイベント発火時に引数なしで呼ばれるもの
Eventは引数ありで呼ばれるものです。

Signalを使ったサンプル

import Foundation
import EmitterKit

class Car{
    let runSignal = Signal()
    func run(){
        runSignal.emit()
    }
}

class Driver{
    var car:Car!
    var runCount:Int = 0
    let carListener:Listener!
    
    init(){
        car = Car()
        
        //once sample ---
        
        car.runSignal.once({ _ in
            self.runCount += 1
            println("car runs in once closure")
        })
        car.run()
        println("1st count: \(self.runCount)")
        car.run()
        println("2nd count: \(self.runCount)")
        
        println("-----------")
        
        //on sample ---
        
        self.runCount = 0
        //you must retain Listener returned from on
        self.carListener = car.runSignal.on({ _ in
            self.runCount += 1
            println("car runs in on closure")
        })
        car.run()
        println("1st count: \(self.runCount)")
        car.run()
        println("2nd count: \(self.runCount)")
        
        //After listener is set to nil, events are not emitterred
        self.carListener = nil
        car.run()
        println("3st count: \(self.runCount)")
        car.run()
        println("4nd count: \(self.runCount)")
    }
}

サンプル実行

let driver = Driver()

出力

car runs in once closure
1st count: 1
2nd count: 1
-----------
car runs in on closure
1st count: 1
car runs in on closure
2nd count: 2
3st count: 2
4nd count: 2

イベント発行時に実行するクロージャへはonとonceの2種類の登録方法があります。
onは何回も呼ばれる
onceは1度呼ばれたらおしまい

このとき、注意するのはonを登録したときに返ってくるListener型の変数を、受けとり側がプロパティとして持つ必要があるということです。
onceはその必要はないです。

上のサンプルでも、受け取り側のDriverインスタンスのcarListenerをnilにすると、それ以降はonであってもクロージャーは呼ばれません。

引数つけてよぶ Event

サンプル

import Foundation
import EmitterKit

class Calculator{
    let addEvent = Event<Int>()
    var result:Int = 0
    
    func add(num:Int){
        result += num
        addEvent.emit(self.result)
    }
}

class CalcUser{
    let calc = Calculator()
    let calcListener:Listener!
    init(){
        self.calcListener = self.calc.addEvent.on{ (result:Int) in
            println("calc is added \(result)")
        }
        self.calc.add(10)
        self.calc.add(5)
    }
}

サンプル実行

let user = CalcUser()

出力

calc is added 10
calc is added 15

Eventの引数はGenericsなので、どんな型でもOKです。(文法用語が正しいかわかんないけど)
この場合はInt型を引数にとるようにしました。

また、emitter, on, onceは引数を2つとることも可能です。
その場合は、第一引数に対象オブジェクト(AnyObject)、第二引数にemitter, on/once間でやりとりする変数を設定できます。

複雑な例

今度は複数のオブジェクトが1つのオブジェクトの複数のプロパティを監視する例です。
emit, on に2つの引数をとって、プロパティ名による振り分けを行いました。

で、ここでちょっと詰まったのですが、第一引数がオブジェクトの場合はそれほど問題ないのですが、文字列にしたいとき、クラスプロパティだとだめで、さらにletなインスタンスプロパティだけでもだめで、それをさらにStringでキャストしないと駄目でした。これは何でなのかはわかりません、、。ソースコードを読んでみたらHelpers.swiftの中にgetHashというメソッドがあり、これがString(“foo”) と “foo” を区別しているっぽいのですが、どうなんでしょう。
このあたり、AnyObjectとAnyという違いがあるのかもしれない、、。うーん。

ともかくサンプルです。

import Foundation
import EmitterKit

class Person{
    let PROP_AGE = String("age")
    let PROP_TITLE = String("title")
    let propEvent = Event<(target:AnyObject, value:Any)>()
    
    init(title:String, age:Int){
        self.title = title
        self.age = age
    }
    
    var title:String{
        didSet{
            propEvent.emit(self.PROP_TITLE, (self, self.title))
        }
    }
    
    var age:Int{
        didSet{
            propEvent.emit(self.PROP_AGE, (self, self.age))
        }
    }
}

class Client{
    var listeners:[String:Listener!] = [:]
    var name:String
    var targetPerson:Person!
    
    init(name:String, targetPerson:Person){
        self.name = name
        self.targetPerson = targetPerson
        self.registerEvents()
    }
    
    func registerEvents(){
        let propTitle = self.targetPerson.PROP_TITLE
        self.listeners[propTitle] = self.targetPerson.propEvent.on(propTitle, { (target:AnyObject, value:Any) in
            println("person's title changed:\n\tclient: \(self.name), target: \(target), value: \(value)")
        })
        let propAge = self.targetPerson.PROP_AGE
        self.listeners[propAge] = self.targetPerson.propEvent.on(propAge, { (target:AnyObject, value:Any) in
            println("person's age changed:\n\tclient: \(self.name), target: \(target), value: \(value)")
        })
    }
}

class ComplexSampleMain{
    init(){
        
        let person = Person(title: "CEO", age: 61)
        let client1 = Client(name: "Taro", targetPerson:person)
        let client2 = Client(name: "Hanako", targetPerson:person)
        
        person.title = "COO"
        person.age = 48
    }
}

サンプル実行

let m = ComplexSampleMain()

出力

person's title changed:
client: Taro, target: EmitterKitSample.Person, value: COO
person's title changed:
client: Hanako, target: EmitterKitSample.Person, value: COO
person's age changed:
client: Taro, target: EmitterKitSample.Person, value: 48
person's age changed:
client: Hanako, target: EmitterKitSample.Person, value: 48

EmitterKitの悩ましいところは、onでイベント監視するときに必ず監視側がListenerオブジェクトを保持する必要があるということです。(繰り返しになるのですが、onceは必要ないです。)なので、監視するプロパティが多いとその分Listenerオブジェクトも増えていきます。そこで、Dictionaryを作ってそいつで複数登録・管理をするようにしてみました。

で、もし監視の必要がなくなればそのDictinaryの該当部分を削除すれば大丈夫です。

またemitterと on/once間でやりとりするのは、classやTupleなどもやりとりできます。なので、この例ではTupleを使ってやっていますが、それらプロパティをまとめてやりとりするクラスを作ってみるのもいいと思います。

そのほかの機能

この他に、NSObjectのサブクラスであれば、今回のようにwillSet/didSetなどをつかわなくてもプロパティ監視ができるみたいです。
あとNSNotificationCenterを便利に使えるみたいです。

LINEで送る
Pocket

自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る