LoginSignup
33
34

More than 5 years have passed since last update.

Swiftでイベントをタイプセーフに送受信できるライブラリEventHubを書きました。

Last updated at Posted at 2016-03-20

iOSアプリの開発で、あるオブジェクトから他のオブジェクトへイベントを送ったりそれを受け取ったりしたい場合、通常NSNotificationCenterを使うと思います。

しかし使い勝手がよいとはあまり言えないのではないでしょうか。
理由としては...

  • タイプセーフじゃない
    • 送る側の userInfo: のデータの種類を変えても受け取る側がコンパイルエラーにならない
  • 引数が多い
    • object: って何? 送るオブジェクトを指定するんじゃないの?
  • 利用が終わったら removeObserver: しないといけない
    • 忘れがち

なので上記を解決する EventHub というライブラリを作ってみました。

ライブラリ作成にあたってこの記事を大いに参考にさせてもらいました。
EventCenter: AndroidのEventBusのSwift版っぽいのを作りました - Qiita

また、 SwiftEventBus やAndroidの EventBus からも影響を受けています。

リポジトリ

https://github.com/mishimay/EventHub
実体: https://github.com/mishimay/EventHub/blob/master/Pod/Classes/EventHub.swift
テスト: https://github.com/mishimay/EventHub/blob/master/Example/Tests/Tests.swift

インストール

CocoaPodsに登録されています。

use_frameworks!
pod "EventHub"

環境

Swift 2.1

EventHubの特徴

  • タイプセーフ
    • EventType というプロトコルを準拠させた class, struct, enum を送受信する
    • それらオブジェクトにデータを持たせればデータの送受信をタイプセーフに行うことが可能
  • 管理が楽
    • observerであるオブジェクトが破棄されたらEventHubからも自動的に削除される
    • よって removeObserver: などのメソッドを呼ぶ必要がない
  • 軽量
    • 実体はSwiftファイル1つだけ

簡単な例

struct MessageEvent: EventType {
    let message: String
}

EventHub.addObserver(self) { (event: MessageEvent) in
    print(event.message) // -> 😜
}
EventHub.post(MessageEvent(message: "😜"))

使い方

  1. イベントを定義
    EventTypeプロトコルに準拠させてください。
    class でも struct でも enum でも構いません。
    ここで定義したイベントのオブジェクトをpostしたり受け取ったりします。

    enum LoginEvent: EventType {
        case Success(id: String)
        case Failure(error: ErrorType)
    }
    
  2. observerを追加
    addObserver(observer:thread:block:) メソッドを呼びます。

    • observer: イベントを受け取るするオブジェクト。もしこのオブジェクトが破棄されたらEventHubからも自動的に削除されて block: で渡す処理も実行されなくなります。
    • thread: 指定は任意です (デフォルト値はnil)。どのスレッドで処理を実行するか指定します。もしnilだった場合、処理はイベントがpostされたスレッドと同じスレッドで同期的に実行されます。
    • block: イベントを受け取ったときに実行されるクロージャです。postされたイベントオブジェクトを受け取ります。
    EventHub.addObserver(self, thread: .Main) { (event: LoginEvent) in
        switch event {
        case .Success(let id):
            print(id)
        case .Failure(let error):
            print(error)
        }
    }
    
  3. イベントを送信

    EventHub.post(LoginEvent.Success(id: id))
    

コードの説明

内部ではNSNotificationCenterを利用せず、自前で送受信の仕組みを書いています。

// EventTypeプロトコルの定義です。
// 制約はありません。
// EventTypeプロトコルを用意した理由としては、もしどんなオブジェクトでpostできるようにすると
// 「渡すオブジェクトの型が同じだけどイベントの種類が違う」ものがある場合にそれらのイベントが
// 区別しにくくなると考えたからです。
public protocol EventType {}

// どのスレッドでobserveするかを指定するためのenumです。
// .Backgroundを指定する場合、associated value にqueueを指定できます。
// e.g. .Background(queue: dispatch_queue_create("com.example", nil))
public enum Thread {
    case Main
    case Background(queue: dispatch_queue_t?)

    private var queue: dispatch_queue_t {
        switch self {
        case .Main:
            return dispatch_get_main_queue()
        case .Background(let queue):
            return queue ?? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
        }
    }
}

public struct EventHub {

    // 登録されたobserver情報を管理するためのstructです。
    private struct Observation {
        weak var observer: AnyObject?
        let thread: Thread?
        let block: Any
    }

    // 登録されたobserver情報を配列で持っておきます。
    private static var observations = [Observation]()

    // observerの登録
    public static func addObserver<T: EventType>(observer: AnyObject, thread: Thread? = nil, block: T -> ()) {
        observations.append(Observation(observer: observer, thread: thread, block: block))
    }

    // observerの解除
    // 任意のタイミングでobserverを解除したい場合に使用します。
    public static func removeObserver(observer: AnyObject) {
        observations = observations.filter { $0.observer! !== observer }
    }

    // イベントの送信
    public static func post<T: EventType>(event: T) {

        // まずobserverがnilになっている (=破棄されている) ものを取り除きます。
        observations = observations.filter { $0.observer != nil }

        // イベントの型が一致するblockに対してイベントを送信します。
        observations.forEach {
            if let block = $0.block as? T -> () {

                // スレッド指定があればそのスレッドで非同期的に、
                // スレッド指定がなければ同じスレッドで同期的にblockを実行します。
                if let queue = $0.thread?.queue {
                    dispatch_async(queue) {
                        block(event)
                    }
                } else {
                    block(event)
                }
            }
        }
    }

}

おわりに

結構便利なものができたんじゃないかと思います。
最初NSNotificationCenterを使って作ろうと思いましたがむしろ使わないほうが簡単に書けました。
これからどんどん使っていきたいです。

もともとEventKitという名前でしたが、同名のiOSのフレームワークがあったのでEventHubに変えました。詳しくはコメント欄を参照してください。

33
34
4

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