2

I might be misunderstanding a couple of key concepts, but not seeing how to properly handle view bindings and retain proper MVVM structure with SwiftUI.

Let's take this example of two fields that affect the text above them:

struct ContentView: View {
    @State var firstName =  "John"
    @State var lastName = "Smith"

    var body: some View {
        VStack {
            Text("first name: \(firstName)")
            Text("last name: \(lastName)")

            ChangeMeView(firstName: $firstName, lastName: $lastName)
        }

    }

}

struct ChangeMeView: View {
    @Binding var firstName: String
    @Binding var lastName: String

    var body: some View {
        VStack {
            TextField("first name", text: $firstName)
            TextField("last name", text: $lastName)
        }
    }

}

Works as expected. However, if I wanted to follow MVVM, wouldn't I need to move (firstName, lastName) to a ViewModel object within that view? enter image description here

That means that the view starts looking like this:

struct ContentView: View {
    @State var firstName =  "John"
    @State var lastName = "Smith"

    var body: some View {
        VStack {
            Text("first name: \(firstName)")
            Text("last name: \(lastName)")

            ChangeMeView(firstName: $firstName, lastName: $lastName)
        }

    }

}

struct ChangeMeView: View {
//    @Binding var firstName: String
//    @Binding var lastName: String

    @StateObject var viewModel: ViewModel

    init(firstName: Binding<String>, lastName: Binding<String>) {
        //from https://stackoverflow.com/questions/62635914/initialize-stateobject-with-a-parameter-in-swiftui#62636048
        _viewModel = StateObject(wrappedValue: ViewModel(firstName: firstName, lastName: lastName))
    }


    var body: some View {
        VStack {
            TextField("first name", text: viewModel.firstName)
            TextField("last name", text: viewModel.lastName)
        }
    }

}


class ViewModel: ObservableObject {
    var firstName: Binding<String>
    var lastName: Binding<String>

    init(firstName: Binding<String>, lastName: Binding<String>) {
        self.firstName = firstName
        self.lastName = lastName
    }
}

This works but feels to me like it might be hacky. Is there another smarter way to pass data (like bindings) to a view while retaining MVVM?

Here's an example where I try using @Published. While it runs, the changes don't update the text:

struct ContentView: View {
     var firstName =  "John"
     var lastName = "Smith"

    var body: some View {
        VStack {
            Text("first name: \(firstName)")
            Text("last name: \(lastName)")

            ChangeMeView(viewModel: ViewModel(firstName: firstName, lastName: lastName))
        }

    }

}

struct ChangeMeView: View {
    @ObservedObject var viewModel: ViewModel

    init(viewModel: ViewModel) {
        self.viewModel = viewModel
        //apple approved strategy from https://stackoverflow.com/questions/62635914/initialize-stateobject-with-a-parameter-in-swiftui#62636048
//        _viewModel = StateObject(wrappedValue: ViewModel(firstName: firstName, lastName: lastName))
    }


    var body: some View {
        VStack {
            TextField("first name", text: $viewModel.firstName)
            TextField("last name", text: $viewModel.lastName)
        }
    }

}


class ViewModel: ObservableObject {
    @Published var firstName: String
    @Published var lastName: String

    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}
user1408431
  • 69
  • 2
  • 8
  • No Binding is only for SwiftUI views – lorem ipsum Dec 15 '22 at 02:18
  • You would remove the `@State` and `@Binding` properties. Your view model should be an an `ObservableObject` with `@Published` properties. You pass an instance of your view model to your view's initialiser – Paulw11 Dec 15 '22 at 02:27
  • @Paulw11 - I updated the last example with your suggestions. Not quite getting that to work. – user1408431 Dec 15 '22 at 03:00
  • 1
    In your updated example you don't have a shared, published property between your `ContentView` and your `ChangeMeView`. You could share the view model instance or you can make things more complicated and implement full MVVM - Where you have a model and two view models and the view models are responsible for updating and exposing changes from the model. Compare what you have with the diagram in your question; You have a view model, but no model. SwiftUI tends to work best when you really decompose your views, but creating a separate view model for each view can be overkill. – Paulw11 Dec 15 '22 at 04:55

1 Answers1

0

You are missing the model part of MVVM. In a simple example, as you have here, you probably don't need full MVVM - You can just share a view model between your two views.

However, here is how you can use a model and a view model. The model and view model classes first:

The model just declares two @Published properties and is an @ObservableObject

Model

class Model: ObservableObject {
    
    @Published var firstName: String = ""
    @Published var lastName: String = ""
    
}

The ContentViewModel is initialised with the Model instance and simply exposes the two properties of the model via computed properties.

ContentViewModel

class ContentViewModel {
    
    let model: Model
    
    var firstName:String {
        return model.firstName
    }
    var lastName:String {
        return model.lastName
    }
    
    init(model: Model) {
        self.model = model
    }
    
}

ChangeMeViewModel is a little more complex - It needs to both expose the current values from the Model but also update the values in the Model when the values are set in the ChangeMeViewModel. To make this happen we use a custom Binding. The get methods are much the same as the ContentViewModel - They just accesses the properties from the Model instance. The set method takes the new value that has been assigned to the Binding and updates the properties in the Model

ChangeMeViewModel

class ChangeMeViewModel {
    
    let model: Model
    
    var firstName: Binding<String>
    var lastName: Binding<String>
    
    init(model: Model) {
        self.model = model
        self.firstName = Binding(
            get: {
                return model.firstName
            },
            set: { newValue in
                model.firstName = newValue
            }
        )
        
        self.lastName = Binding(
            get: {
                return model.lastName
            },
            set: { newValue in
                model.lastName = newValue
            }
        )
    }
    
}

Finally we need to create the Model in the App file and use it with the view models in the view hierarchy:

App

@main
struct MVVMApp: App {
    @StateObject var model = Model()
    var body: some Scene {
        WindowGroup {
            ContentView(viewModel: ContentViewModel(model: model))
        }
    }
}

ContentView

struct ContentView: View {
    
    let viewModel: ContentViewModel
    
    var body: some View {
        VStack {
            Text("first name: \(self.viewModel.firstName)")
            Text("last name: \(self.viewModel.lastName)")
            
            ChangeMeView(viewModel:ChangeMeViewModel(model:self.viewModel.model))
        }
    }
}

ChangeMeView

struct ChangeMeView: View {
    
    let viewModel: ChangeMeViewModel
    
    var body: some View {
        VStack {
            TextField("first name", text: self.viewModel.firstName)
            TextField("last name", text: self.viewModel.lastName)
        }
    }
}
Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • Thanks for taking the time to write it up! The example I posted was meant to show where I was lost. The actual view I'm working on has a decent list of bindings, and I was trying to see if I could encapsulate that better within a view model. Leveraging getters and setters here make a ton of sense, and might help remove some of the clutter – user1408431 Dec 16 '22 at 04:06