ローソンデジタルイノベーション(LDI)のiOS/Android開発マネージャーの阪口です。
今回は、NotificationCenter.publisher のイベントを Swift Concurrency でハンドリングする方法についてご紹介します。
開発環境
macOS:Ventura 13.6
Xcode:15.0
サポートOS:iOS14〜
背景
NotificationCenter には Swift Concurrency で簡単に通知をハンドリングできるメソッドがあります。
しかし、このメソッドはiOS15から導入された機能のため、iOS15未満では利用できません。
そのため、iOS14向けに Swift Concurrency で NotificationCenter の通知をハンドリングできるように対応する必要がありました。
対応方法
iOS13から利用可能な NotificationCenter の publisher メソッド(Combine向けのメソッド)を利用して、Swift Concurrency でも NotificationCenter の通知をハンドリングできるように対応していきます。
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に興味が出てきた方、または応援いただける方は是非「読者になる」で応援していただけますと嬉しいです!