Help us understand the problem. What is going on with this article?

イベントリスナー/Swift勉強

More than 5 years have passed since last update.

Swiftのお勉強ということでイベントリスナー的なクラスを書いてみました。
- generic
- protocol
- closure
- operator overloading
あたりを試した感じです。

リスナークラス

まずイベント通知を受け取るリスナークラス

// イベントの通知を受け取る
class EventListener <EventArgType>{
    typealias EventMethod = (sender: AnyObject!, args: EventArgType!) -> ()
    typealias IdType = UInt64

    let invoke: EventMethod!
    internal var id: IdType? // 登録状況やリスナーを識別するための情報 nilなら未登録

    init( callback: EventMethod! ){
        self.invoke = callback
        self.id = nil
    }
}

イベント管理

次にリスナーの登録先、イベントの発行のためのクラス

// 登録されたリスナーにイベントを通知する
class Event <EventArgType, LockType where LockType : LockProtocol>{
    typealias SelfType = Event<EventArgType, LockType>
    typealias ListenerType = EventListener<EventArgType>
    typealias IdType = EventListener<EventArgType>.IdType

    private var listeners = Dictionary<IdType, EventListener<EventArgType>>()
    private var uniqueId : IdType = 0 // id発行に使う
    private var locker = LockType()

    init(){
    }

    // リスナー登録
    func add(listener : ListenerType) -> Bool{
        var result = false;
        locker.lock()
        result = addImpl(listener)
        locker.unlock()
        return result
    }
    // リスナー削除
    // ToDo: removeの為にlistener覚えておくのはもったいないのでIdにしたほうがいいかも
    func remove(listener : ListenerType) -> Bool{
        var result = false;
        locker.lock()
        result = removeImpl(listener)
        locker.unlock()
        return result
    }
    // イベント発行
    func trigger(sender: AnyObject!, args: EventArgType!){
        locker.lock()
        triggerImpl(sender, args : args )
        locker.unlock()
    }

    private func addImpl(listener : ListenerType) -> Bool{
        if listener.id != nil { return false }
        let id = issueId()
        listeners[ id ] = listener
        listener.id = id
        return true;
    }

    private func removeImpl(listener : ListenerType) -> Bool{
        if listener.id == nil { return false }
        listeners.removeValueForKey(listener.id!)
        listener.id = nil
        return true;
    }

    private func triggerImpl(sender: AnyObject!, args: EventArgType!){
        for listener in listeners.values{
            listener.invoke( sender: sender, args: args )
        }
    }

    // リスナーを識別するためのID発行
    private func issueId() -> IdType{
        do {
            uniqueId++
            if listeners[uniqueId] == nil { return uniqueId }
        }while(true) // ToDo
    }
}

単純に add(), remove() でリスナーの付け外しをして
trigger()でイベントを通知するといったもの

protocolの検証として同期機構

protocolのお試しがてら同期のための仕組みも書いてみる。
同期の必要がなければ空定義のクラスを使えばよいといったもの

protocol LockProtocol
{
    init()
    func lock()
    func unlock()
}
// なにもしない(同期の必要がない場合に利用する)
class DummyLock : LockProtocol
{
    required init(){}
    func lock(){}
    func unlock(){}
}

使い方

var myEventStr = Event<String, DummyLock>() // 同期なし、文字列通知を受け取るイベント

var strListener1 = EventListener<String>( { sender, args in println("hi \(args)") })
var strListener2 = EventListener<String>( { sender, args in println("hello \(args)") })
var strListener3 = EventListener<String>( { sender, args in println("bonjour \(args)") })

myEventStr.add( strListener1 )
myEventStr.add( strListener2 )
myEventStr.add( strListener3 )
myEventStr.remove( strListener2 )

myEventStr.trigger(sender, args: "hoge") // senderは任意
hi hoge
bonjour hoge

演算子オーバーロード

myEventStr += strListener2

みたいに書きたい場合は globalスコープで定義。クラス内で書くとエラーになってかなり悩まされました。generic引数を書き直さないといけない(?)感じでうーん

// add event(operator)
func +=<EventArgType, LockType where LockType : LockProtocol> ( 
        inout handler : Event<EventArgType, LockType>, 
        event : EventListener<EventArgType>)
{ 
    handler.add( event ) 
}
// remove event(operator)
func -=<EventArgType, LockType where LockType : LockProtocol> ( 
        inout handler : Event<EventArgType, LockType>, 
        event : EventListener<EventArgType>)
{ 
    handler.remove( event ) 
}

演算子オーバーロード時のエラー対処は…?

上記例ではadd()/remove()では戻り値で成功・失敗を知ることができますが、+=や-=は結果を放置しています。
C#は例外で通知する作りですが、Swiftの場合は演算子オーバーロード関数内でエラーになったらどうするのがいいんでしょうね…

おまけ:AutoLockみたいなもの

C++だとコンストラクタとデストラクタで lock(), unlock() を呼ぶ AutoLockクラスみたいなのがあって、一行でスコープ範囲の保護をするなんてのがありますけど、SwiftはARCだから deinit が呼ばれるタイミングがコントロールできなくて、この方法使えないんですね…。

Swiftのautoreleasepool等で注意する点

で紹介されていたclosureを使う方法とかになるようで。


C#の

timer.Tick += new EventHandler(timer_tick);
timer.Tick -= new EventHandler(timer_tick);

みたいに情報を保持することをしなくてもリスナーの add / remove ができると便利なんですが…さて

#Objective-C未経験でいきなりSwiftから始めるとAPI探しが大変ですね
#playground様々だけどXCode6正式版でもよく落ちる…

Maoku
ゲームつくってご飯食べてる人 C/C++/C#がメイン Mac買ったのでSwiftのお勉強中
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away