Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

EnumではじめるNSNotification管理

iOSエンジニアのすーです!
7月に入り、かなり暑くなってきましたね。
「pokemon go」も日本で配信されてかなりホットな感じになっていますね...。 前回の記事とはちょっと変わってネタが小さくなりますが、Swiftの Enum でNSNotificationを扱いやすくするTipsを書こうと思います。

Enumを使ってNSNotificationで使うkeyを定義する

SwiftのEnumObjective-Cのそれとは違って、要素がStringの Enum を作ることができるので

enum UserNotification: String {
    case DidLogIn
    case DidLogOut
    case DidReload
}

こんな感じでわかりやすくまとめることができます。
Objective-Cの時は

// .h ファイル
extern NSString * const UserDidLogInNotification;
extern NSString * const UserDidLogOutNotification;
// .m ファイル
NSString * const UserDidLogInNotification = @"UserDidLogInNotification";
NSString * const UserDidLogOutNotification = @"UserDidLogOutNotification";

みたいな感じで定義していましたね...!

これを実際に使う時は、 EnumrawValue を用います。

// observerの登録
NSNotificationCenter.DefaultCenter().addObserver(
    self, 
    selector: #selector(self.userDidLogIn(_:)),
    name: UserNotification.DidLogIn.rawValue
    object: nil
)

// 通知を送信
NSNotificationCenter.DefaultCenter().postNotificationName(
    UserNotification.DidLogIn.rawValue,
    object: user,
    userInfo: userInfo
)

// keyを指定して解除する場合
NSNotificationCenter.defaultCenter().removeObserver(
    self, 
    name: UserNotification.DidLogIn.rawValue
)

...うーん。 確かに、 Enum でまとめるのは良いけれど、使い勝手がよくなったとはちょっと言えないですね。

そこで...

EnumにNSNotificationを扱いやすくする関数を追加する

Swiftの Enum には関数を定義することができるので、これを使って、先ほどの処理を包み込みます。

enum UserNotification: String {
    case DidLogIn
    case DidLogOut
    case DidReload

    func addObserver(observer: AnyObject, selector: Selector, object: AnyObject? = nil) {
        NSNotificationCenter.defaultCenter().addObserver(observer, selector: selector, name: self.rawValue, object: object)
    }
    
    func addObserver(object: AnyObject? = nil, queue: NSOperationQueue? = nil, usingBlock block: NSNotification -> Void) -> NSObjectProtocol {
        return NSNotificationCenter.defaultCenter().addObserverForName(self.rawValue, object: object, queue: queue, usingBlock: block)
    }

    
    func post(object: AnyObject? = nil, userInfo: [NSObject: AnyObject]? = nil) {
        NSNotificationCenter.defaultCenter().postNotificationName(self.rawValue, object: object, userInfo: userInfo)
    }
    
    func removeObserver(observer: AnyObject, object: AnyObject? = nil) {
        NSNotificationCenter.defaultCenter().removeObserver(observer, name: self.rawValue, object: object)
    }
}

これで、

UserNotification.DidLogIn.addObserver(self, selector: #selector(self.userDidLogIn(_:)))

UserNotification.DidLogIn.post(user, userInfo)

UserNotification.DidLogIn.removeObserver(self)

といった形でスッキリ書くことができます...! 短いのは正義ですね
スッキリ書けるようにするために、一部objectやuserInfoに対して Default Valueを渡して、引数の省略ができるようにしています。

protocolとextensionを用いてより汎用的に

ただ、このままだと汎用性がなく、

enum UploadNotification: String {
    case WillStart
    case DidStart
    case Complete
    case Failed

    // ここにさっきの関数を追加するのはしんどい...
}

みたいな別のNotificationを表すEnumが来た時に先ほどの処理をコピーして...ってなりかねないので、
protocolextensionを用いて 汎用的にしてみます。

protocol NSNotificationObservable: RawRepresentable {
    var rawValue: String { get }
}

extension NSNotificationObservable {
    func addObserver(observer: AnyObject, selector: Selector, object: AnyObject? = nil) {
        NSNotificationCenter.defaultCenter().addObserver(observer, selector: selector, name: self.rawValue, object: object)
    }
    
    func addObserver(object: AnyObject? = nil, queue: NSOperationQueue? = nil, usingBlock block: NSNotification -> Void) -> NSObjectProtocol {
        return NSNotificationCenter.defaultCenter().addObserverForName(self.rawValue, object: object, queue: queue, usingBlock: block)
    }

    
    func post(object: AnyObject? = nil, userInfo: [NSObject: AnyObject]? = nil) {
        NSNotificationCenter.defaultCenter().postNotificationName(self.rawValue, object: object, userInfo: userInfo)
    }
    
    func removeObserver(observer: AnyObject, object: AnyObject? = nil) {
        NSNotificationCenter.defaultCenter().removeObserver(observer, name: self.rawValue, object: object)
    }
}

このように定義して、先ほどの Enum 達に、NSNotificationObservable を適応してあげると、

// NSNotificationObservableを適応する
extension UserNotification: NSNotificationObservable {}
extension UploadNotification: NSNotificationObservable {}

// どちらも同じように関数を呼び出すことができる!
UserNotification.DidLogIn.addObserver(self, selector: #selector(self.userDidLogIn(_:)))
UserNotification.DidLogIn.post(user, userInfo)
UserNotification.DidLogIn.removeObserver(self)

UploadNotification.Complete.addObserver(self, selector: #selector(self.uploadDidComplete(_:)))
UploadNotification.Complete.post(user, userInfo)
UploadNotification.Complete.removeObserver(self)

こんな感じで protocol を適応するだけでサクッと使えるようになります!嬉しい。
protocolの定義の時点で、 RawRepresentable を継承するように指定しているので、Enum以外にこのprotocolを適応することができないようになっています。 また、

protocol NSNotificationObservable {
    var rawValue: String { get }
}

と宣言しているおかげで、間違って String型以外のEnumに適応してしまう...なんて事も防げます。 (Int型のEnumでは、rawValueInt 型となってしまうので、衝突してしまいます。)

ちなみに

最初は、以下のような感じで NSNotificationCenter を拡張して使うことを考えていたのですが、結局あの長ったらしい記述から逃れられなくて諦めました。
こちらの方が普通の拡張だなあという感じですが。

public extension NSNotificationCenter {
    func addObserver<Key: RawRepresentable where Key.RawValue == String>(observer: AnyObject, selector aSelector: Selector, key: Key?, object anObject: AnyObject?) {
        self.addObserver(observer, selector: aSelector, name: key?.rawValue, object: anObject)
    }
    func postNotificationKey<Key: RawRepresentable where Key.RawValue == String>(key: Key, object anObject: AnyObject?) {
        self.postNotificationName(key.rawValue, object: anObject)
    }
    func postNotificationKey<Key: RawRepresentable where Key.RawValue == String>(key: Key, object anObject: AnyObject?, userInfo aUserInfo: [NSObject : AnyObject]?) {
        self.postNotificationName(key.rawValue, object: anObject, userInfo: aUserInfo)
    }
    
    func removeObserver<Key: RawRepresentable where Key.RawValue == String>(observer: AnyObject, key: Key?, object anObject: AnyObject?) {
        self.removeObserver(observer, name: key?.rawValue, object: anObject)
    }
    
    
    func addObserverForKey<Key: RawRepresentable where Key.RawValue == String>(key: Key?, object obj: AnyObject?, queue: NSOperationQueue?, usingBlock block: (NSNotification) -> Void) -> NSObjectProtocol {
        return self.addObserverForName(key?.rawValue, object: obj, queue: queue, usingBlock: block)
    }
}

まとめ

NSNotificationを使った通知を使う時は、大体 NSNotificationCenter.defaultCenter()から始まり、長い記述であれこれ書くのですが、 Enum を使ったアプローチで通知のkeyをまとめるのに加えて、addObserver/post/removeObserver周りをスッキリさせてみました。

今回のコードはGistにもあげています。

積極採用中!!

子育て家族アプリFamm、カップル専用アプリPairyを運営する Timers inc. では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP : http://timers-inc.com/engineerings

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる