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

1

7

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)
    }

 }
Zebadiah answered 11/8, 2021 at 15:55 Comment(7)
@Binding should only be used inside `Views. It would be easier to help if your question included the view layer as well.Braga
Are you sure you want a Binding? You're trying to use sink on it, which is what comes from a PublisherIngamar
No, I'm not sure if I need binding. What I'd like to achieve is to connect two classes together, in a way that when a sourceproperty gets written to (by the view), Logic class will react on that change and update it's own properties.Zebadiah
I agree with the top comment -- probably good to include the view layer. It definitely sounds like you don't need the binding -- probably just pass the publisher to Logic. But, I'm struggling to see why you need nested ObservableObjects as well. That seems suspicious. I get why your ViewModel would be an ObservableObject, but not why Logic would. And right now, it's the reverse in your code.Ingamar
Added simplified View Layer as an exampleZebadiah
Where is the ViewModel owned? And why is Logic and ViewModel both an ObservableObject? Neither is really addressed by the view layer that got added.Ingamar
1. VM is owned by the view. 2. Feel free to remove ObservableObject from one of the classes.Zebadiah
B
9

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)
    }
}
Braga answered 11/8, 2021 at 16:13 Comment(2)
Perfect, this looks like it achieves the goal I had in mind. Thanks!Zebadiah
you just solved my annoying memory leak :)Valdavaldas

© 2022 - 2024 — McMap. All rights reserved.