Reputation: 92
I have FirstViewModel class with @Published property "myProperty". In view FirstView I insert SecondView which has environmentObject SecondViewModel. SecondViewModel also has own @Published var myProperty. myProperty in FirstViewModel and SecondViewModel must be the same (trigger each other). How I can pass myProperty of FirstViewModel into SecondViewModel for bindint to SecondView. SecondViewModel and SecondView must not know about FirstViewModel and FirstView.
import SwiftUI
import Combine
class FirstViewModel: ObservableObject {
@Published var myProperty: String?
// Some code and some methods
// In this class I handle "myProperty"
private var cancellables = Set<AnyCancellable>()
init() {
self.myProperty = "Two"
}
}
struct FirstView: View {
@EnvironmentObject var vm: FirstViewModel
var body: some View {
SecondView()
.environmentObject(SecondViewModel(myProperty: $vm.myProperty))
}
}
class SecondViewModel: ObservableObject {
@Published var myProperty: String?
// Some code
}
struct SecondView: View {
@EnvironmentObject var vm: SecondViewModel
let items: [String] = ["One", "Two", "Three"]
var body: some View {
Picker(
selection: $vm.myProperty,
label: EmptyView()
) {
ForEach(vm.items, id: \.self) { item in
Text(item).tag(item)
}
}
}
}
Another words I need pass @Published var myProperty from FirstViewModel to SecondViewModel for binding to SecondView.
This code don't work, I got error "Cannot convert value of type 'Binding<String?>' to expected argument type 'String?'"
Upvotes: 3
Views: 1563
Reputation: 1222
This is how you pass around a published variable to another view model, and remember to clean the cancellable FirstViewModel
class FirstViewModel: ObservableObject {
@Published var myProperty: String?
var secondViewModel: SecondViewModel {
.init(myProperty: $myProperty)
}
}
SecondViewModel
class SecondViewModel: ObservableObject {
@Published var myProperty: String?
var subscriptions = Set<AnyCancellable>()
init(myProperty: Published<String?>.Publisher) {
myProperty
.receive(on: DispatchQueue.main)
.assign(to: \.myProperty, on: self)
.store(in: &subscriptions)
}
}
Upvotes: 0
Reputation: 21
In views, you listen to published properties using @StateObject or @ObservedObject, for classes however you use combine to listen to a published property from another class.
In the first ViewModel:
In the second ViewModel:
class FirstViewModel
{
@Published var firstProperty
static let shared = FirstViewModel()
}
class SecondViewModel
{
@Published secondProperty
private var cancellables = Set<AnyCancellable>()
private let firstViewModel = FirstViewModel.shared
init() {
setupSubscribers()
}
func setupSubscribers() {
firstViewModel.$firstProperty.sink { [weak self] firstProperty in
self?.secondProperty = firstProperty
}
.store(in: &cancellables)
}
}
Upvotes: 0
Reputation: 31
In order for them to be in sync and be able to make changes to each other, I think they would have to know about each other somehow or at least one or the other has to know about the other.
The way I see it, SecondView
only exists under FirstView
, so FirstView
will exist as long as SecondView
exists. Therefore, it may be a good idea to use dependency injection to inject FirstViewModel
(which also only exists as long as FirstView
exists) as a dependency into SecondView
like so:
struct FirstView: View {
...
var body: some View {
SecondView()
.environmentObject(SecondViewModel())
.environmentObject(FirstViewModel())
}
}
struct SecondView: View {
@EnvironmentObject var firstViewModel: FirstViewModel
@EnvironmentObject var viewModel: SecondViewModel
...
}
One thing you might be able to do is create an extension to help sync to bindable properties, which just requires them both to conform to the Equatable
protocol (and Strings do conform to this protocol) like so:
extension View {
func sync<Bindable: Equatable>(_ published: Binding<Bindable>, with binding: Binding<Bindable>) -> some View {
self
.onChange(of: published.wrappedValue) { published in
binding.wrappedValue = published
}
.onChange(of: binding.wrappedValue) { binding in
published.wrappedValue = binding
}
}
}
You can then make use of this in your code like so:
struct SecondView: View {
@EnvironmentObject var firstViewModel: FirstViewModel
@EnvironmentObject var viewModel: SecondViewModel
...
var body: some View {
Picker(
...
) {
...
}
.sync($firstViewModel.myProperty, with: $viewModel.myProperty) // Note the order in which u put the properties u wanna sync doesn't matter
}
}
Let me know if you need any clarification or if this does not work. Happy to help!
Upvotes: 1
Reputation: 52565
Instead of a @Published
property in your second view model, you should use a @Binding
so there's a single source of truth (the first view model):
class FirstViewModel: ObservableObject {
@Published var myProperty: String
init() {
self.myProperty = "Two"
}
}
struct FirstView: View {
@EnvironmentObject var vm: FirstViewModel
var body: some View {
TextField("", text: $vm.myProperty)
SecondView()
.environmentObject(SecondViewModel(myProperty: $vm.myProperty))
}
}
class SecondViewModel: ObservableObject {
@Binding var myProperty: String
init(myProperty: Binding<String>) {
_myProperty = myProperty
}
}
struct SecondView: View {
@EnvironmentObject var vm: SecondViewModel
var body: some View {
TextField("", text: vm.$myProperty)
}
}
I've simplified the example a little bit so that it compiles. You can easily change the types back from String
to String?
if you remove the TextField
s that I just have to show the behavior.
Probably worth mentioning (since otherwise it will almost certainly appear in the comments) that there are some that will disagree with the architecture choice of using multiple view models like this, but I think that's outside of the scope of the question, which I took at face value.
Upvotes: 1