iosswiftswiftuicombineobserver-pattern

SwiftUI, Combine: pass @Published property as a @Binding to another object (delegation style)


I have a following synthetic example:

final class MainViewModel: ObservableObject {
    @Published var response: String?

    func makeSecondaryViewModel() -> SecondaryViewModel {
        SecondaryViewModel(response: $response) // Error
    }
}

final class SecondaryViewModel: ObservableObject {
    @Binding var response: String?

    init(response: Binding<String?>) {
        self._response = response
    }

    func performRequest() {
        // Make request here
        response = "The result of the request"
    }
}

I'm getting an error when trying to initialize SecondaryViewModel with the response binding:

Cannot convert value of type 'Published<String?>.Publisher' to expected argument type 'Binding<String?>'

enter image description here

I understand the error, so I have 2 questions:

  1. What's the right way to make a @Binding that would target a property on another ObservableObject?
  2. What would be the best way to connect these two ViewModels in a delegation-like relationship, maybe my approach is completely wrong?

In case of an external configuration, i.e. if I pass the response as a binding in a SwiftUI View, I can actually get this idea to work:

SecondaryViewModel(response: $mainViewModel.response)

The problem is that I cannot do this while being inside the `MainViewModel:

SecondaryViewModel(response: $self.mainViewModel.response) // doesn't work

Solution

  • You can pass @Published around and use @Binding to maintain a single source of truth across various Views. However, IMO, I don't think this is the right way to do. I would rather passing ViewModel within Views instead. Something like this:

    struct MySecondView: View {
        //Or inject via EnvironmentObject if you want
        @StateObject private var viewModel: SecondaryViewModel
        
        init(viewModel: SecondaryViewModel) {
            self._viewModel = .init(wrappedValue: viewModel)
        }
        
        var body: some View {
            ...
        }
    }
    
    struct MyMainView: View {
        @StateObject private var viewModel = MainViewModel()
        
        var body: some View {
            NavigationStack {
                NavigationLink("Move") {
                    MySecondView(viewModel: .init(response: $viewModel.response))
                }
            }
        }
    }