2

I have a couple of nested views with nested viewModels that I need to ensure validation on.

Essentially ViewA has a form a Submit button and a Subview (ViewB) - the ViewModel for ViewB is contained within ViewB This architecture cannot change due to boring business reasons ViewB has some form fields. I need to ensure all the fields are valid in ViewA and ViewB before the Submit button is enabled.

I can't seem to figure out how to do this as any of the changes in ViewB are not triggering a refresh of ViewA

Here's some simplified code as a rough example

    class ViewModelB: ObservableObject {
    @Published var text = ""
    @Published var textTwo = ""
    
    func isValid() -> Bool {
        !text.isEmpty && !textTwo.isEmpty
    }
}

class ViewModelA: ObservableObject {
    @Published var text = ""
    @ObservedObject var addressViewModel: ViewModelB = ViewModelB()

    func isValid() -> Bool {
        print("is Valid called")
        return !text.isEmpty && addressViewModel.isValid()
    }
}
struct AddressView: View {
    @ObservedObject var viewModel: ViewModelB

    init(addressModel:ViewModelB) {
        self.viewModel = addressModel
    }
    var body: some View {
        TextField("View B Text", text: $viewModel.text)
        TextField("View B TextTwo", text: $viewModel.textTwo)
    }
}

struct ContentView: View {
    @StateObject var viewModel: ViewModelA = ViewModelA()

    var body: some View {
        VStack {
            TextField("View A Text", text: $viewModel.text)
            AddressView(addressModel: viewModel.addressViewModel)
            Button("Apply Button") {
                print("Button tapped!")
            }.disabled(!viewModel.isValid() || !viewModel.addressViewModel.isValid())
        }
    }
}
user499846
  • 681
  • 3
  • 11
  • 24
  • `@StateObject` is for when you need a reference type in an `@State` it is not designed to be used as a legacy UIKit style view model object. To use SwiftUI effectively you'll need to learn the `View` struct and its `body`, `let`, `@Binding` and `@State` features. – malhal Apr 06 '23 at 15:25

3 Answers3

2

Changing ViewModelB in AddressView to be Binding not ObservedObject, as well, as making it in ViewModelA Published instead of ObservedObject solves the issue.

The reason for this is that SwiftUI's mechanism for observing changes only functions on one level of objects I guess, and doesn't automatically spread updates to objects that are nested within.

Please find full code below.

class ViewModelB: ObservableObject {
    @Published var text = ""
    @Published var textTwo = ""
    
    func isValid() -> Bool {
        !text.isEmpty && !textTwo.isEmpty
    }
}

class ViewModelA: ObservableObject {
    @Published var text = ""
    @Published var addressViewModel: ViewModelB = ViewModelB()
    
    func isValid() -> Bool {
        print("is Valid called")
        return !text.isEmpty && addressViewModel.isValid()
    }
}

struct AddressView: View {
    @Binding var viewModel: ViewModelB
    
    var body: some View {
        TextField("View B Text", text: $viewModel.text)
        TextField("View B TextTwo", text: $viewModel.textTwo)
    }
}

struct ContentView: View {
    @StateObject var viewModel: ViewModelA = ViewModelA()
    
    var body: some View {
        VStack {
            TextField("View A Text", text: $viewModel.text)
            AddressView(viewModel: $viewModel.addressViewModel)
            Button("Apply Button") {
                print("Button tapped!")
            }.disabled(!viewModel.isValid() || !viewModel.addressViewModel.isValid())
        }
    }
}
0

You should add addressViewModel in ContentView for the view to refresh when it changes, or you can follow this answer: How to tell SwiftUI views to bind to nested ObservableObjects

Also note that we use @ObservedObject exclusively in SwiftUI Views.

Timmy
  • 4,098
  • 2
  • 14
  • 34
-1

Try to use this:

@Published var addressViewModel: ViewModelB = ViewModelB()

Instead of this:

@ObservedObject var addressViewModel: ViewModelB = ViewModelB()
NexusUA
  • 217
  • 1
  • 3
  • 13
  • Thank you,but this doesn't work unfortunately, the disabled state is still present on the button when all the textfields are filled out – user499846 Apr 05 '23 at 15:21
  • 1
    I forgot to add @Binding, but I see you resolved this issue, so it's good! :) – NexusUA Apr 06 '23 at 07:25