ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

NotificationCenter の publisher を Swift Concurrency でハンドリング対応

ローソンデジタルイノベーション(LDI)のiOS/Android開発マネージャーの阪口です。

今回は、NotificationCenter.publisher のイベントを Swift Concurrency でハンドリングする方法についてご紹介します。

開発環境

macOS:Ventura 13.6

Xcode:15.0

サポートOS:iOS14〜

背景

NotificationCenter には Swift Concurrency で簡単に通知をハンドリングできるメソッドがあります。

developer.apple.com

しかし、このメソッドはiOS15から導入された機能のため、iOS15未満では利用できません。

そのため、iOS14向けに Swift Concurrency で NotificationCenter の通知をハンドリングできるように対応する必要がありました。

対応方法

iOS13から利用可能な NotificationCenter の publisher メソッド(Combine向けのメソッド)を利用して、Swift Concurrency でも NotificationCenter の通知をハンドリングできるように対応していきます。

developer.apple.com

1. Combine から Swift Concurrency へ処理を変換するラッパークラスを作成

まず、Combine の イベントを async/await でハンドリングできるようにするためのラッパークラスを作成します。

///
/// Combine の イベントを async/await でハンドリングできるようにするためのラッパークラス
///
class CombineAsyncStream<Element> {
    private var continuation: AsyncThrowingStream<Element, Error>.Continuation?
    private var asyncStream: AsyncThrowingStream<Element, Error>?

    init(bufferingPolicy: AsyncThrowingStream<Element, Error>.Continuation.BufferingPolicy = .bufferingNewest(1), _ buildCombine: @escaping @Sendable(AsyncThrowingStream<Element, Error>.Continuation) -> AnyCancellable) {
        asyncStream = AsyncThrowingStream<Element, Error>(bufferingPolicy: bufferingPolicy, { innerContinuation in
            continuation = innerContinuation
            let cancellable = buildCombine(innerContinuation)
            innerContinuation.onTermination = { _ in cancellable.cancel() }
        })
    }

    deinit {
        cancel()
    }

    func wait() async throws -> Element? {
        try await asyncStream?.first { _ in true }
            .flatMap { element in
                cancel()
                return element
            }
    }

    func cancel() {
        continuation?.finish()
        asyncStream = nil
        continuation = nil
    }
}

2. CombineAsyncStream を継承した NotificationCenter.publisher 用のクラスを定義

先程作成したラッパークラス(CombineAsyncStream)を継承し、NotificationCenter.publisher のハンドリングに特化したクラスを定義します。

final class AsyncNotification: CombineAsyncStream<Notification> {
    convenience init(for name: Notification.Name, object: AnyObject? = nil) {
        self.init { continuation in
            NotificationCenter.default
                .publisher(for: name, object: object)
                .sink(receiveValue: { notification in
                    continuation.yield(notification)
                })
        }
    }
}

3. AsyncNotification を利用して NotificationCenter の通知を待つ処理を実装

AsyncNotification.wait() を呼ぶことで、async 関数内で NotificationCenter の通知が来るまで処理を止めることができます。 そのため、Swift Concurrency で簡単に通知をハンドリングすることができます。

また、 AsyncNotification.cancel() を呼ぶと、 通知待ち状態をキャンセルすることができます。

利用例
func fetchMessages() async throws -> [Message] {
    let notification = AsyncNotification(for: NSNotification.Name.xxx)

    if refreshMessages() {
        // リフレッシュが実行されたら、NotificationCenter の通知が来た後にメッセージ取得処理を実施する
        _ = try? await notification.wait()
        return getMessages()
    } else {
        // リフレッシュが実行されなかったら、NotificationCenter の購読をキャンセルし、メッセージを取得する
        notification.cancel()
        return getMessages()
    }
}

まとめ

今回紹介した方法を利用することで、Swift Concurrency で NotificationCenter の通知のハンドリングできるようになります。

また、 CombineAsyncStream は NotificationCenter に限らず Combine のイベントを Swift Concurrency で 簡単にハンドリングできるため、より柔軟に非同期処理を実装することが可能になります。

最後に

LDIに興味が出てきた方、または応援いただける方は是非「読者になる」で応援していただけますと嬉しいです!