Search code examples
swiftcombineswift-playground

Sink.receive() Does Not Get called After Declaring an Initial Value in Combine Swift


Issue:

I am using a Publisher in a code example which I wrote based on the code example here, [https://developer.apple.com/documentation/combine/observableobject][1]. I expect the print statement inside the body of the sink.receive() to print "received value 30" when I run the code sample below followed by "received value 31". What is happening instead is that the console is printing "received value ()" then "asaad has birthday ()".

Why is this? How can I fix it?

Code:

import Combine

class Contact: ObservableObject {
    @Published var name: String
    @Published var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    func hasBirthday() {
        age += 1
    }
}

var cancellable: AnyCancellable!

let asaad = Contact(name: "Asaad Jaber", age: 30)

cancellable = asaad.objectWillChange
    .sink(receiveValue: { value in
        print("received value \(value)")
    })

let hasBirthday = asaad.hasBirthday()

print("asaad has birthday \(hasBirthday)")

Steps to Reproduce:

  1. I clicked on Xcode in my dock to launch Xcode.
  2. I clicked File > New > Playground to create a new playground.
  3. I copied the code in the code example and pasted it in the playground and I modified some of the values.
  4. I clicked the Play button to execute the code.

Solution

  • You are listening on the object itself and that it changed, not on the value that you want to observe changes on. You could change your code to the following:

    cancellable = asaad.$age //<-- listen to the publisher of age
        .sink(receiveValue: { value in
            print("received value \(value)")
        })
    

    Then you get updates whenever age changes and thus the value will also be the new value of age.

    Edit: Explanation why the solution in your question does not work as you expect.

    If you subscribe to changes of the entire object via objectWillChange, you need to consider following points.

    • You will get an event when any published var changes on the object. If you change age, there is an event. If you change name, there is an event. That means: the event can't just also send the new value, because it wouldn't know what type that value has. Is it Int? A String? Any other custom type you implemented your own? Could be anything. (Also, say you have another Int property called height. If you just got any Int value in the sink: you would not know "did the age just change or the height?")
    • That property is just giving an event "something will change". Looking at the type of value in the sink, you will also see: value is Void. There's no further information attached. You only get to know that something on that object is about to change. Not what exactly will change.
    • Looking at the wording of "object will change" also signals that you cannot just access the new the values in the object itself inside of the sink. That event is fired before the object changes, not after it changed.

    That's why it's better to listen to specific property publishers like $age.

    • When listening to changes of age, the sink knows that it's observing changes of an Int.
    • That will allow you write a sink that knows "do this if the age changes". And on another property you will be able to define "do another thing when the the height changes." And so on.