Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

iOSでNotificationを非同期で送ろう

はじめに

こんにちは。グノシー事業部でiOSの開発を担当している hongmhoonです。

この記事はGunosy Advent Calendar 2018の11日目の記事です。 昨日はtoshimaruさんのCircleCI 2.0/2.1の機能をフル活用してCI/CDワークフローを改善してみたでした。

Notificationの基本

NotificationCenter.default.post(name: .blogDeadlineDidCome, object: nil)

iOSで何かのイベントをアプリ全体的に送信するには上のように書くのが基本です。

こちらは同期的に処理されますが、呼出元の処理が遅延するなどの理由で送信のタイミングをずらしたい時もあります。

Notificationの非同期送信

非同期でNotificationを送信するにはNotificationQueue にNotificationをenqueueします。

以下のコードはasyncNotification1を先にNotificationQueue にenqueueしてからsyncNotificationNotificationCenterで送信してますが、出力を見るとSyncNotificationが先に同期で処理されてAsyncNotification1は呼出メソッドの処理が終わった後非同期で処理されます。

全コードはここにあります。

    func postNotification() {
        print("postNotification start")

        print("asyncNotification1 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification1), postingStyle: .whenIdle)
        print("asyncNotification1 did post")

        print("syncNotification will post")
        NotificationCenter.default.post(name: .syncNotification, object: nil)
        print("syncNotification did post")

        print("postNotification end")
    }
    
// 出力
postNotification start
asyncNotification1 will post
asyncNotification1 did post
syncNotification will post
NSNotificationName(_rawValue: SyncNotification)
syncNotification did post
postNotification end
NSNotificationName(_rawValue: AsyncNotification1)

Notificationの送信タイミング設定

NotificationQueueenqueueメソッドにはpostingStyleパラメータがありこれで送信タイミングの設定が可能です。

  • .now : 同期送信
  • .asap : 呼出メソッドの処理が終わった直後に送信
  • .whenIdle : run loopが暇になったら送信

以下のコードは.whenIdle -> .asap -> .now の順にenqueueしましたが、.now は同期で処理されて.whenIdle.asapより遅く処理されます。

    func postNotification() {
        print("postNotification start")

        print("asyncNotification1 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification1), postingStyle: .whenIdle)
        print("asyncNotification1 did post")

        print("asyncNotification2 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification2), postingStyle: .asap)
        print("asyncNotification2 did post")

        print("asyncNotification3 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification3), postingStyle: .now)
        print("asyncNotification3 did post")

        print("syncNotification will post")
        NotificationCenter.default.post(name: .syncNotification, object: nil)
        print("syncNotification did post")

        print("postNotification end")
    }
    
// 出力
postNotification start
asyncNotification1 will post
asyncNotification1 did post
asyncNotification2 will post
asyncNotification2 did post
asyncNotification3 will post
NSNotificationName(_rawValue: AsyncNotification3)
asyncNotification3 did post
syncNotification will post
NSNotificationName(_rawValue: SyncNotification)
syncNotification did post
postNotification end
NSNotificationName(_rawValue: AsyncNotification2)
NSNotificationName(_rawValue: AsyncNotification1)

Notificationの重複除外

NotificationQueueenqueueメソッドにはcoalesceMaskパラメータがありこれで重複除外の設定が可能です。

  • .none : 重複除外しない
  • .onName : 同じ名前のNotificationは除外
  • .onSender : 同じSenderのNotificationは除外

onName

以下のコードはasyncNotification1asyncNotification2を2回ずつenqueueしましたがcoalesceMask: .onNameを使ったasyncNotification1は一回のみ処理されます。

    func postNotification() {
        print("postNotification start")

        print("asyncNotification1 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification1), postingStyle: .whenIdle)
        print("asyncNotification1 did post")

        print("asyncNotification1 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification1), postingStyle: .whenIdle, coalesceMask: .onName, forModes: nil)
        print("asyncNotification1 did post")

        print("asyncNotification2 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification2), postingStyle: .whenIdle)
        print("asyncNotification2 did post")

        print("asyncNotification2 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification2), postingStyle: .whenIdle, coalesceMask: .none, forModes: nil)
        print("asyncNotification2 did post")

        print("postNotification end")
    }
    
// 出力
postNotification start
asyncNotification1 will post
asyncNotification1 did post
asyncNotification1 will post
asyncNotification1 did post
asyncNotification2 will post
asyncNotification2 did post
asyncNotification2 will post
asyncNotification2 did post
postNotification end
NSNotificationName(_rawValue: AsyncNotification1)
NSNotificationName(_rawValue: AsyncNotification2)
NSNotificationName(_rawValue: AsyncNotification2)

onSender

以下のコードはasyncNotification1を3回enqueueしましたがcoalesceMask: .onSenderの影響で一回のみ処理されます。

    func postNotification() {
        print("postNotification start")

        print("asyncNotification1 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification1), postingStyle: .whenIdle)
        print("asyncNotification1 did post")

        print("asyncNotification2 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification2), postingStyle: .whenIdle)
        print("asyncNotification2 did post")

        print("asyncNotification3 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification3), postingStyle: .whenIdle, coalesceMask: .onSender, forModes: nil)
        print("asyncNotification3 did post")

        print("postNotification end")
    }
    
// 出力
postNotification start
asyncNotification1 will post
asyncNotification1 did post
asyncNotification2 will post
asyncNotification2 did post
asyncNotification3 will post
asyncNotification3 did post
postNotification end
NSNotificationName(_rawValue: AsyncNotification1)

Enqueue済みNotificationの削除

NotificationQueuedequeueNotificationsメソッドに削除したいNotificationを設定することで送信前のNotificationの削除が可能です。

以下のコードはasyncNotification1asyncNotification2をenqueueしましたがdequeueされたasyncNotification1は処理されず、asyncNotification2のみ処理されます。

    func postNotification() {
        print("postNotification start")

        print("asyncNotification1 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification1), postingStyle: .whenIdle)
        print("asyncNotification1 did post")

        print("asyncNotification1 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification2), postingStyle: .whenIdle)
        print("asyncNotification1 did post")

        NotificationQueue.default.dequeueNotifications(matching: .init(name: .asyncNotification1),
                                                       coalesceMask: Int(NotificationQueue.NotificationCoalescing.onName.rawValue))

        print("postNotification end")
    }
    
// 出力
postNotification start
asyncNotification1 will post
asyncNotification1 did post
asyncNotification1 will post
asyncNotification1 did post
postNotification end
NSNotificationName(_rawValue: AsyncNotification2)

終わりに

端末の性能向上やRecativeなこの頃なのであまり使う機会はないかも知らないですが参考になれたら幸いです。

明日はiOSネタではなくて、技術ブログの始め方になる予定なので、引き続きGunosy Advent Calendar 2018をお楽しみください。

参考

サンプルコード

import UIKit

class ViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default.addObserver(self, selector: #selector(observeSyncNotification(notification:)), name: .syncNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(observeAsyncNotification1(notification:)), name: .asyncNotification1, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(observeAsyncNotification2(notification:)), name: .asyncNotification2, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(observeAsyncNotification3(notification:)), name: .asyncNotification3, object: nil)

        postNotification()
    }

    func postNotification() {
        print("postNotification start")

        print("asyncNotification1 will post")
        NotificationQueue.default.enqueue(.init(name: .asyncNotification1), postingStyle: .whenIdle)
        print("asyncNotification1 did post")

        print("syncNotification will post")
        NotificationCenter.default.post(name: .syncNotification, object: nil)
        print("syncNotification did post")

        print("postNotification end")
    }
    
    @objc func observeSyncNotification(notification: Notification) {
        print(notification.name)
    }

    @objc func observeAsyncNotification1(notification: Notification) {
        print(notification.name)
    }
    
    @objc func observeAsyncNotification2(notification: Notification) {
        print(notification.name)
    }
    
    @objc func observeAsyncNotification3(notification: Notification) {
        print(notification.name)
    }

}
extension Notification.Name {
    public static let syncNotification = Notification.Name("SyncNotification")
    public static let asyncNotification1 = Notification.Name("AsyncNotification1")
    public static let asyncNotification2 = Notification.Name("AsyncNotification2")
    public static let asyncNotification3 = Notification.Name("AsyncNotification3")
}