I'm having an issue with a Binding property inside of a view model that's being created in another view model. Here is simplified version of 2 views:
struct ContentView: View {
@StateObject var viewModel = ContentViewModel()
var body: some View {
VStack {
// binding through `@Published var toggleViewModel`
ToggleWrapperView(viewModel: viewModel.toggleViewModel)
// binding through `var isOnBinding: Binding<Bool>`
ToggleWrapperView(viewModel: ToggleViewModel(isOn: viewModel.isOnBinding!, title: "Binding through isOnBinding"))
// binding through `@Published var isOn`
ToggleWrapperView(viewModel: ToggleViewModel(isOn: $viewModel.isOn, title: "Binding through view"))
}
.padding()
}
}
struct ToggleWrapperView: View {
var viewModel: ToggleViewModel
var body: some View {
VStack {
Toggle(isOn: viewModel.$isOn, label: {
Text(viewModel.title + " \(viewModel.isOn)")
})
}
}
}
And here are simple view models for the views:
class ContentViewModel: ObservableObject {
@ObservedObject var dataManager = DataManager()
@Published var toggleViewModel: ToggleViewModel!
@Published var isOn: Bool = false // Property for the binding in the ContentView
private var privateIsOnForBinding: Bool = false
var isOnBinding: Binding<Bool>?
init() {
toggleViewModel = ToggleViewModel(isOn: $dataManager.isOn, title: "Binding from DataManager")
initBinding()
}
func initBinding() {
isOnBinding = Binding {
self.privateIsOnForBinding
} set: { updatedValue in
self.privateIsOnForBinding = updatedValue
print(self.isOnBinding!)
}
}
}
struct ToggleViewModel {
@Binding var isOn: Bool
let title: String
}
class DataManager: ObservableObject {
@Published var isOn: Bool = false {
didSet {
print("did set DataManager.isOn \(isOn)")
}
}
}
From the ToggleWrapperView
examples above only binding created with $viewModel.isOn
is working as expected on toggle action.
In the example with toggleViewModel
I can see that the property in DataManager
is being updated (print in the property observer), but the binding itself stays false
Binding<Bool>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.LocationBox<SwiftUI.(unknown context at $1c5c1c4dc).ObjectLocation<BindingTest.DataManager, Swift.Bool>>, _value: false)
And using var isOnBinding: Binding<Bool>
gives me the same result, binding is not being updated
Binding<Bool>(transaction: SwiftUI.Transaction(plist: []), location: SwiftUI.LocationBox<SwiftUI.FunctionalLocation<Swift.Bool>>, _value: false)
This is just a simplified example, but I'd like to understand why 2 versions don't work.
I can make the example with toggleViewModel
to work if I mark var viewModel: ToggleViewModel
as @Binding
in the ToggleWrapperView
, and remove the @Binding
from the isOn
property of ToggleViewModel
(first example in the view).
However I'm trying to have the binding for the isOn
between ToggleViewModel
and DataManager
that's being passed in the ContentViewModel
if possible.
I don't have any simplified approach for you. I can tell you two things.
First, to pass a view model to a SwiftUI view, do the following:
- Have the view model conform to ObservableObject.
- Add @Published properties in the view model for any properties where you want the view to update when the property's value changes.
- Use @StateObject in the view that owns the view model.
- Use @ObservedObject in the other views where you want to use the view model.
Second, avoid nesting observable objects. SwiftUI views may not update properly when a property in a nested observable object has its value change. If you are unfamiliar with nested observable objects, read the following article:
https://holyswift.app/how-to-solve-observable-object-problem/