'How to create Viewmodel for subview and update values on change in Swiftui?
Currently i am using subview without using viewmodel and which is woking fine..UI is updating on value change. (find code below) but i want to create viewmodel for subview and update UI on value change..
Normal code without viewmodel
struct MainView: View {
@State private var selectedTag: String?
var body: some View {
VStack {
ForEach(products, id: \.description) { item in
SubView(productTag: item.productId, selectedTag: self.$selectedTag)
}
}
}
}
struct SubView: View {
var productTag: String?
@Binding var selectedTag: String?
var body: some View {
Button(action: {
self.selectedTag = self.productTag
})
}
}
with viewmodel (but not working for me - UI is not updating)
struct MainView: View {
@State private var selectedTag: String?
var body: some View {
VStack {
ForEach(products, id: \.description) { item in
SubView(viewModel: SubViewViewModel(productTag: item.productId ?? "", selectedTag: self.selectedTag ?? ""))
}
}
}
}
struct SubView: View {
private var viewModel: SubViewViewModel
var body: some View {
Button(action: {
viewModel.selectedTag = viewModel.productTag
})
}
}
class SubViewViewModel: ObservableObject {
var productTag: String
@Published var selectedTag: String?
init(productTag: String, selectedTag: String) {
self.productTag = productTag
self.selectedTag = selectedTag
}
}
I might missing some concept, kindly suggest the solution for same.
Solution 1:[1]
If you want SubView to alter MainView via a shared model you will first need to initialize it as a @StateObject on the MainView, because MainView is the owner.
Then you pass that same viewModel to SubView as an @ObservedObject since SubView is only borrowing it.
struct MainView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
ForEach(products, id: \.description) { item in
SafHelpCard(viewModel: viewModel, productTag: item.productId)
}
}
.onAppear {
viewModel.onAppear(/* whatever setup is needed */)
}
}
}
struct SubView: View {
@ObservedObject var viewModel: ViewModel
let productTag: String?
var body: some View {
Button(action: {
viewModel.selectedTag = viewModel.productTag
})
}
}
class ViewModel: ObservableObject {
@Published var selectedTag: String?
func onAppear(args:...) {
// do required setup in here
}
}
Solution 2:[2]
Make your view model a singleton.
struct MainView: View {
@StateObject private var viewModel = ViewModel.shared
var body: some View {
VStack {
ForEach(products, id: \.description) { item in
SafHelpCard(viewModel: viewModel, productTag: item.productId)
}
}
.onAppear {
viewModel.onAppear(/* whatever setup is needed */)
}
}
}
struct SubView: View {
@ObservedObject var viewModel: ViewModel.shared
let productTag: String?
var body: some View {
Button(action: {
viewModel.selectedTag = viewModel.productTag
})
}
}
class ViewModel: ObservableObject {
@Published var selectedTag: String?
static var shared = ViewModel()
func onAppear(args:...) {
// do required setup in here
}
}
Solution 3:[3]
You can pass the view model as an environment object to your subview like this or use a Binding variable as well.
Since you are using @State, which only works with simple data types. For the view to be updated as per your view model, you need to use @StateObject as viewmodel is a complex data type.
Please check this link: https://levelup.gitconnected.com/state-vs-stateobject-vs-observedobject-vs-environmentobject-in-swiftui-81e2913d63f9
class ViewModel: ObservableObject {
@Published var name: String = "Test"
}
struct MainView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
ChildView()
.environmentObject(viewModel)
}
}
struct ChildView: View {
@EnvironmentObject var model: ViewModel
var body: some View {
Text(model.name)
}
}
Solution 4:[4]
You shouldn't create ViewModel on SubView to update value from MainView, but rather pass down ViewModel from MainView to SubView
class MainViewModel: ObservableObject {
@Published var selectedTag: String?
var products: [Product]
}
struct MainView: View {
@StateObject private var viewModel = MainViewModel()
var body: some View {
List {
ForEach(viewModel.products, id: \.description) { item in
SubView(viewModel: viewModel, product: item)
}
}
}
}
struct SubView: View {
@ObservedObject var viewModel: MainViewModel
var product: Product
var body: some View {
Button(product.productTag) {
mainModel.selectedTag = product.productTag
}.onAppear {
// Do can do things to viewModel when SubView on appear here
}
}
}
If your SubView has some business that doesn't want to bother MainView, and you only need selectedTag from MainView, you should keep using @Binding and create a @StateObject private var viewModel inside SubView
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | user3069232 |
| Solution 3 | Rajat |
| Solution 4 |
