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

109 Views Asked by At

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.
3

There are 3 best solutions below

2
JanMensch On BEST ANSWER

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.
1
jasim On

Currently, hasBirthday returning Void. You would need to return Bool on the function hasBirthday(). You could do like

func hasBirthday()-> Bool { age += 1 return true }

Now if the birthday function would return true and you have the birthday

0
Sweeper On

objectWillChange is an ObservableObjectPublisher, which has an Output type of Void. Therefore, printing value will output (), the string representation of Void.

objectWillChange is only published once, because you only change age once. Also, it publishes before the change, so if you did:

.sink(receiveValue: { value in
    print("received value \(asaad.age)")
})

You will see the old value of 30.

If you just want to observe changes to age, subscribe to the $age publisher instead.

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

Lastly, asaad has birthday () is printed because you wrote print("asaad has birthday \(hasBirthday)"). Note that hasBirthday is also a Void, because hasBirthday doesn't return a value. I'm not sure what you are expecting it to return here.