Search code examples
swiftcombine

sink value is Void with publisher


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.


Solution

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