Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Combine: Going from Notification Center addObserver with selector to Notification publisher

Tags:

swift

combine

I've seen how to transition to Combine using a Publisher from some NotificationCenter code, but have not seen how to do it for something like:

        NotificationCenter.default.addObserver(
        self,
        selector: #selector(notCombine),
        name: NSNotification.Name(rawValue: "notCombine"),
        object: nil
    )

I've seen that this is available as a Publisher, but I don't have a selector and am not sure what to do for it:

        NotificationCenter.default.publisher(
        for: Notification.Name(rawValue: "notCombine")
    )

Does anyone know? Thanks!

like image 370
SRMR Avatar asked Oct 25 '19 13:10

SRMR


2 Answers

You're right to say "I don't have a selector", as that is half the point right there. You can receive notifications from the notification center without a selector using Combine.

The other half of the point is that you can push your logic for dealing with the notification up into the Combine pipeline, so that the correct result just pops out the end of the pipeline if it reaches you at all.

The old-fashioned way

Let's say I have a Card view that emits a virtual shriek when it is tapped by posting a notification:

static let tapped = Notification.Name("tapped")
@objc func tapped() {
    NotificationCenter.default.post(name: Self.tapped, object: self)
}

Now let's say, for purposes of the example, that what the game is interested in when it receives one of these notifications is the string value of the name property of the Card that posted the notification. If we do this the old-fashioned way, then getting that information is a two-stage process. First, we have to register to receive notifications at all:

NotificationCenter.default.addObserver(self, 
    selector: #selector(cardTapped), name: Card.tapped, object: nil)

Then, when we receive a notification, we have to look to see that its object really is a Card, and if it is, fetch its name property and do something with it:

@objc func cardTapped(_ n:Notification) {
    if let card = n.object as? Card {
        let name = card.name
        print(name) // or something
    }
}

The Combine way

Now let's do the same thing using the Combine framework. We obtain a publisher from the notification center by calling its publisher method. But we don't stop there. We don't want to receive a notification if the object isn't a Card, so we use the compactMap operator to cast it safely to Card (and if it isn't a Card, the pipeline just stops as if nothing had happened). We only want the Card's name, so we use the map operator to get it. Here's the result:

let cardTappedCardNamePublisher = 
    NotificationCenter.default.publisher(for: Card.tapped)
        .compactMap {$0.object as? Card}
        .map {$0.name}

Let's say that cardTappedCardNamePublisher is an instance property of our view controller. Then what we now have is an instance property that publishes a string if a Card posts the tapped notification, and otherwise does nothing.

Do you see what I mean when I say that the logic is pushed up into the pipeline?

So how would we arrange to receive what comes out of the end of the pipeline? We could use a sink:

let sink = self.cardTappedCardNamePublisher.sink {
    print($0) // the string name of a card
}

If you try it, you'll see that we now have a situation where every time the user taps a card, the name of the card is printed. That is the Combine equivalent of our earlier register-an-observer-with-a-selector approach.

like image 159
matt Avatar answered Oct 17 '22 08:10

matt


The use case is not entirely clear, but here a basics playground example:

import Combine
import Foundation

class CombineNotificationSender {

    var message : String

    init(_ messageToSend: String) {
        message = messageToSend
    }

    static let combineNotification = Notification.Name("CombineNotification")
}

class CombineNotificationReceiver {
    var cancelSet: Set<AnyCancellable> = []

    init() {
        NotificationCenter.default.publisher(for: CombineNotificationSender.combineNotification)
            .compactMap{$0.object as? CombineNotificationSender}
            .map{$0.message}
            .sink() {
                [weak self] message in
                self?.handleNotification(message)
            }
            .store(in: &cancelSet)
    }

    func handleNotification(_ message: String) {
        print(message)
    }
}

let receiver = CombineNotificationReceiver()
let sender = CombineNotificationSender("Message from sender")

NotificationCenter.default.post(name: CombineNotificationSender.combineNotification, object: sender)
sender.message = "Another message from sender"
NotificationCenter.default.post(name: CombineNotificationSender.combineNotification, object: sender)

For some use cases you can also make it a combine only solution without using notifications

import Combine
import Foundation

class CombineMessageSender {
    @Published var message : String?
}

class CombineMessageReceiver {
    private var cancelSet: Set<AnyCancellable> = []

    init(_ publisher: AnyPublisher<String?, Never>) {
        publisher
            .compactMap{$0}
            .sink() {
                self.handleNotification($0)
            }
            .store(in: &cancelSet)
    }

    func handleNotification(_ message: String) {
        print(message)
    }
}

let sender = CombineMessageSender()
let receiver = CombineMessageReceiver(sender.$message.eraseToAnyPublisher())
sender.message = "Message from sender"
sender.message = "Another message from sender"
like image 21
berni Avatar answered Oct 17 '22 10:10

berni