Skip to content

Instantly share code, notes, and snippets.

@fxm90
Last active December 5, 2022 15:15
Show Gist options
  • Save fxm90/68e2cc3cd6b63751c225b1e1249088cc to your computer and use it in GitHub Desktop.
Save fxm90/68e2cc3cd6b63751c225b1e1249088cc to your computer and use it in GitHub Desktop.
Extension for "NotificationCenter" to observe a notification just once and directly unsubscribe.
//
// NotificationCenter+ObserveOnce.swift
//
// Created by Felix Mau on 18.10.20.
// Copyright © 2020 Felix Mau. All rights reserved.
//
import UIKit
extension NotificationCenter {
/// Adds an observer to the given notification center, which fires just once.
///
/// Note:
/// - Same parameters as "addObserver", but with default properties
/// See http://apple.co/2zZIYJB for details.
///
/// Parameters:
/// - name: The name of the notification for which to register the observer
/// - object: The object whose notifications the observer wants to receive
/// - queue: The operation queue to which block should be added.
/// - block: The block to be executed when the notification is received.
func observeOnce(forName name: NSNotification.Name?,
object obj: Any? = nil,
queue: OperationQueue? = nil,
using block: @escaping (Notification) -> Swift.Void) {
var observer: NSObjectProtocol?
observer = addObserver(forName: name,
object: obj,
queue: queue) { [weak self] notification in
// Here we directly remove the observer, so this closure will be executed just once.
self?.removeObserver(observer!)
block(notification)
}
}
}
@fxm90
Copy link
Author

fxm90 commented Dec 27, 2017

Testcase

import XCTest

class NotificationCenterObserveOnceTestCase: XCTestCase {

    // MARK: - Private properties

    private var notificationCenter: NotificationCenter!

    // MARK: - Public methods

    override func setUp() {
        super.setUp()

        notificationCenter = NotificationCenter()
    }

    override func tearDown() {
        notificationCenter = nil

        super.tearDown()
    }

    // MARK: - Tests

    func testClosureShouldBeExecutedAfterNotificationIsReceived() {
        // Given
        let expectation = self.expectation(description: "Expect closure to be executed.")

        let notificationName = Notification.Name(rawValue: "FooBar")
        notificationCenter.observeOnce(forName: notificationName) { _ in
            expectation.fulfill()
        }

        // When
        notificationCenter.post(name: notificationName, object: nil)

        // Then
        wait(for: [expectation],
             timeout: TimeInterval(0.1))
    }

    func testClosureShouldBeExecutedJustOnceAfterNotificationIsReceived() {
        // Given
        let notificationName = Notification.Name(rawValue: "FooBar")

        // Setup a counter to validate closure is executed just once.
        var observeOnceCount = 0
        notificationCenter.observeOnce(forName: notificationName) { (_: Notification) in
            observeOnceCount += 1
        }

        // When
        // Trigger multiple notifications
        let notificationQuantity = 10
        for _ in 1 ... notificationQuantity {
            notificationCenter.post(name: notificationName, object: nil)
        }

        // Then
        XCTAssertEqual(observeOnceCount, 1)
    }
}

@fxm90
Copy link
Author

fxm90 commented Oct 5, 2022

When targeting iOS versions >= 13 you can simplify the code by using the combine operator first():

NotificationCenter.default
    .publisher(for: Notification.Name)
    .first()
    .sink { notification in 
        // ...
    }

@DasserBasyouni
Copy link

DasserBasyouni commented Dec 5, 2022

In addition to @fxm90 answer, To avoid the sink() warning (Result of call to 'sink(receiveValue:)' is unused), You can use:

import Foundation
import Combine

// Source + TestCase: https://gist.github.com/fxm90/68e2cc3cd6b63751c225b1e1249088cc
extension NotificationCenter {
    /// Adds an observer to the given notification center, which fires just once.
    ///
    /// Note:
    ///  - Same parameters as "addObserver", but with default properties
    ///    See http://apple.co/2zZIYJB for details.
    ///
    /// Parameters:
    ///  - name:   The name of the notification for which to register the observer
    ///  - queue:  The operation queue to which block should be added.
    ///  - block:  The block to be executed when the notification is received.
    func observeOnce(forName name: NSNotification.Name,
                     queue: OperationQueue? = nil,
                     using block: @escaping (Notification) -> Swift.Void) {
        
        var cancellable: AnyCancellable?
        cancellable = NotificationCenter.default
            .publisher(for: name)
            .receive(on: queue ?? OperationQueue.main)
            .first()
            .sink { 
                block($0) 
                cancellable?.cancel()
            }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment