Search code examples
data-bindingswiftuibindingcombinepublisher

Pass @Published where @Binding is required (SwiftUI, Combine)


A ViewModel class has a sourceProperty that is being edited by the TextField. That property is @Published. I'd like to pass it to the Logic class which has an initializer with Binding<String>. That class will be listening to the sourceProperty changes, react on them and set it's output to the @Published output property.

How can I pass @Published sourceProperty as an initializer parameter to the Logic class?

Relevant code:

final class ViewModel {
    @Published var sourceProperty: String = ""
    private var logic: Logic?

    init() {
        self.logic = Logic(data: $sourceProperty)
        $logic.output.sink({result in 
            print("got result: \(result)")
        })
    }

}

final class Logic: ObservableObject {
    private var bag = Set<AnyCancellable>()
    @Published var output: String = ""
    @Binding var data: String
    init(data: Binding<String>) {
        self._data = data

        $data.sink({ newValue in 
            output = newvalue + "ABCDE"
        }).store(in: &bag)
    }
}

So far I'm getting the following error:

Cannot convert value of type 'Published.Publisher' to expected argument type 'Binding'

The goal is to use a change in the object's own property to trigger a method invocation in another object and then bind that second object's output to some view.

View Layer:

public struct ViewLayer: View {
    @Binding private var sourceProperty: String

    public init(_ placeholder: String,
                sourceProperty: Binding<String>,
    ) {
        self.placeholder = placeholder
        self._sourceProperty = sourceProperty
    }

    public var body: some View {
        TextField(placeholder, text: $sourceProperty)
    }

 }

Solution

  • If I understand your question correctly, you are probably looking for something like that:

    final class ViewModel: ObservableObject {
        
        @Published var sourceProperty: String = ""
        private lazy var logic = Logic(data: $sourceProperty)
        private var cancellable: AnyCancellable?
    
        init() {
            cancellable = logic.$output
                .sink { result in
                    print("got result: \(result)")
                }
        }
    
    }
    
    final class Logic: ObservableObject {
        
        @Published private(set) var output: String = ""
        
        init(data: Published<String>.Publisher) {
            data
                .map { $0 + "ABCDE" }
                .assign(to: &$output)
        }
    }