Home > OS >  sink value is Void with publisher
sink value is Void with publisher

Time:07-10

Consider the below Observable Object.

class User: ObservableObject {
    @Published var age: Int
    @Published var name: String {
        didSet {
            objectWillChange.send()
        }
    }
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

The below code prints blank value or Void block. Any reason why? If we change Integer value age it should simply print that value.

let userJohnCancellable = userJohn.objectWillChange.sink { val in

    print("Changed Value \(val)")
    
}
userJohn.age = 21
userJohn.age = 39

We can try to print the values in the closure using userJohn.age. But why does val not return a Integer value in this case.

Also what would be the best way to handle sink changes for age and name, both, one is String other is Int.

CodePudding user response:

When you look in the documentation for ObservableObject you will find thatobjectWillChange is ObservableObjectPublisher

/// A publisher that emits before the object has changed.
public var objectWillChange: ObservableObjectPublisher { get }

which in turn is defined as having an output of type Void:

final public class ObservableObjectPublisher : Publisher {

    /// The kind of values published by this publisher.
    public typealias Output = Void

    /// The kind of errors this publisher might publish.
    ///
    /// Use `Never` if this `Publisher` does not publish errors.
    public typealias Failure = Never

}

There is no need to send objectWillChange from didSet - each time any of the @Published values changes objectWillChange will emit a value. If you want to get notified when a particular property marked as @Published changes and receive the new value you have to subscribe to that particular property:

let userJohn = User(name: "Johnny", age: 17)

let objectWillChangeCancellable = userJohn
    .objectWillChange
    .sink {
        print("object will change")
    }

let ageCancellable = userJohn
    .$age
    .sink { age in
        print("new value of age is \(age)")
    }

let nameCancellable = userJohn
    .$name
    .sink { name in
        print("new value of name is \(name)")
    }

This will get printed:

new value of age is 17
new value of name is Johnny

if you add:

userJohn.name = "John"

you will see the following printed:

object will change
new value of name is John

if you add:

userJohn.age = 21

you will see the following printed:

object will change
new value of age is 21

CodePudding user response:

You seem to be confused about ObservableObject. It is for use with SwiftUI. But your code is not SwiftUI code, so you don't need ObservableObject and you really can't use it in any meaningful way. If the goal is to be able to subscribe to the properties of User so as to be notified when one of them changes, then it suffices to make those properties Published:

class User {
    @Published var age: Int
    @Published var name: String
    init(age: Int, name: String) {
        self.age = age; self.name = name
    }
}

Here's an example of using it; I will assume we have a user property of a UIViewController:

class ViewController: UIViewController {
    var cancellables = Set<AnyCancellable>()
    var user = User(age: 20, name: "Bill")
    override func viewDidLoad() {
        super.viewDidLoad()
        user.$age.sink {print("age:", $0)}.store(in: &cancellables)
        user.$name.sink {print("name:", $0)}.store(in: &cancellables)
    }
}

If this view controller's user has its age or name changed, you'll see the print output in the console.

If the question is how to handle both changes in a single pipeline, they have different types, as you observe, so you'd need to define a union so that both types can come down the same pipeline:

class User {
    @Published var age: Int
    @Published var name: String
    enum Union {
        case age(Int)
        case name(String)
    }
    var unionPublisher: AnyPublisher<Union, Never>?
    init(age: Int, name: String) {
        self.age = age; self.name = name
        let ageToUnion = $age.map { Union.age($0) }
        let nameToUnion = $name.map { Union.name($0) }
        unionPublisher = ageToUnion.merge(with: nameToUnion).eraseToAnyPublisher()
    }
}

And again, here's an example of using it:

class ViewController: UIViewController {
    var cancellables = Set<AnyCancellable>()
    var user = User(age: 20, name: "Bill")
    override func viewDidLoad() {
        super.viewDidLoad()
        user.unionPublisher?.sink { union in
            switch union {
            case .age(let age): print ("age", age)
            case .name(let name): print ("name", name)
            }
        }.store(in: &cancellables)
    }
}

Again, change the user property's name or age and you'll get an appropriate message in the console.

  • Related