admin管理员组

文章数量:1403229

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?>'

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

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?>'

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
Share Improve this question edited Mar 21 at 8:04 Richard Topchii asked Mar 21 at 7:55 Richard TopchiiRichard Topchii 8,2899 gold badges60 silver badges131 bronze badges 6
  • From a design perspective, the idea is a very bad one. The view model is responsible to provide the published data and it should be the only thing which generates (mutates) this value. In your design, you effectively pass the value to some other thing, which now mutates it. This creates chaos. So, keep the logic which mutates the published value at one place. Also, don't allow others (a view for example or any other artefact) to modify this published data, means make it @Published private(set) var response: String?. The logic is a function, consider to combine the two functions into one. – CouchDeveloper Commented Mar 21 at 9:20
  • 2 Note, @Binding is only meaningful in a View, not in a class such as final class SecondaryViewModel: ObservableObject, remove it. – workingdog support Ukraine Commented Mar 21 at 9:34
  • Try using the View structs as your view model and State/Binding for the data. Use .task(id:) for requests. – malhal Commented Mar 21 at 9:51
  • @malhal What you mention is just an implementation detail. Basically, you can always implement the logic of a view model directly in a view, call it "ViewModelView" ;) which does nothing with pixels by itself, but passes down the state to its children views . and completely replaces the Observable. However, this doesn't answer the OP's question, which is basically where and how the logic and state should be located and computed, i.e. at one place or separated in parent and child. And, by the way, I recently tend to prefer to implement the logic in a SwiftUI view ;) – CouchDeveloper Commented Mar 21 at 10:09
  • @CouchDeveloper that's one of the way to do it, i.e. "component-driven" approach. For this purpose, there is a lot of logic that's better to be extracted somewhere else, the MVVM works perfectly for my app. – Richard Topchii Commented Mar 21 at 10:15
 |  Show 1 more comment

1 Answer 1

Reset to default 0

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

本文标签: