skip to Main Content

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.

4

Answers


  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
        }
    }
    
    Login or Signup to reply.
  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
    }
    }
    
    Login or Signup to reply.
  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)
         }
     }
    
    Login or Signup to reply.
  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

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search