How to Work with Bindings When Using a View Model VS Using @Binding in the View Itself

Share Data Binding Between Class and Struct View

@Published properties work well for sharing data between ObservableObjects and Views.

For example:

class ViewModel : ObservableObject {
struct APIError : Identifiable {
var id = UUID()
var message : String
}

@Published var error : APIError?

func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.error = APIError(message: "Error message")
}
}
}

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

var body: some View {
VStack {
Text("Hello, world!")
.alert(item: $viewModel.error) { item in
Alert(title: Text(item.message))
}
}
.onAppear {
viewModel.apiCall()
}
}
}

You could also do this with a custom Binding, but it's a little messier. The above example would definitely be my go-to.

class ViewModel : ObservableObject {
@Published var errorMessage : String?

var alertBinding : Binding<Bool> {
.init {
self.errorMessage != nil
} set: { newValue in
if !newValue { self.errorMessage = nil }
}

}

func apiCall() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.errorMessage = "Error!"
}
}
}

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

var body: some View {
VStack {
Text("Hello, world!")
.alert(isPresented: viewModel.alertBinding) {
Alert(title: Text(viewModel.errorMessage ?? "(unknown)"))
}
}
.onAppear {
viewModel.apiCall()
}
}
}

Binding ViewModel and TextFields with SwiftUI

I think we can simplify it with below code.

class SignInViewModel: ObservableObject{
@Published var username = ""
@Published var password = ""
}


struct SigninView: View {
@ObservedObject var viewModel = SignInViewModel()

var body: some View {
VStack(alignment: .leading, spacing: 15.0){

TextField("username", text: $viewModel.username)
TextField("password", text: $viewModel.password)

Spacer()

Button("Sign In"){
print("User: \(self.viewModel.username)")
print("Pass: \(self.viewModel.password)")
}
}.padding()
}
}

How to use View Binding on custom views

Just inform the root, and whether you want to attach to it

init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this)
}

or

init { // inflate binding and add as view
binding = ResultProfileBinding.inflate(LayoutInflater.from(context), this, true)
}

which inflate method to use will depend on the root layout type in xml.

Why using View Binding is changing the layout?


If I change setContentView(binding.root) to setContentView(R.layout.activity_main), the layout of Android Studio and the device become the same

Most likely caused by how the container is not passed to the inflater if the view is inflated like this.

You could try the following instead:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root"

And

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
binding = ActivityMainBinding.bind(findViewById(R.id.root))

val rollButton = binding.rollButton
rollButton.setOnClickListener { rollDice() }

SwiftUI - @Binding to a computed property which accesses value inside ObservableObject property duplicates the variable?


If you only look where you can't go, you might just miss the riches below

Breaking single source of truth, and breaching local (private) property of @StateObjectby sharing it via Binding are two places where you can't go.

@EnvironmentObject or more generally the concept of "shared object" between views are the riches below.

This is an example of doing it without MVVM nonsense:

import SwiftUI

final class EnvState: ObservableObject {@Published var value: Double = 0 }

struct ContentView: View {
@EnvironmentObject var eos: EnvState

var body: some View {
VStack{
ViewA()

ViewB()
}
}
}

struct ViewA: View {
@EnvironmentObject var eos: EnvState

var body: some View {
Text("\(eos.value)").padding()
}
}

struct ViewB: View {
@EnvironmentObject var eos: EnvState

var body: some View {
VStack{
Text("\(eos.value)")

Slider(value: $eos.value, in: 0...1)
}
}
}

Isn't this easier to read, cleaner, less error-prone, with fewer overheads, and without serious violation of fundamental coding principles?

MVVM does not take value type into consideration. And the reason Swift introduces value type is so that you don't pass shared mutable references and create all kinds of bugs.

Yet the first thing MVVM devs do is to introduce shared mutable references for every view and pass references around via binding...

Now to your question:

the only options I see are either using only one ViewModel per Model, or having to pass the Model (or it's properties) between ViewModels through Binding

Another option is to drop MVVM, get rid of all view models, and use @EnvironmentObject instead.

Or if you don't want to drop MVVM, pass @ObservedObject (your view model being a reference type) instead of @Binding.

E.g.;

struct ContentView: View {
@ObservedObject var viewModelA = ViewModelA()

var body: some View {
VStack{
ViewA(value: viewModelA)

ViewB(value: viewModelA)
}
}
}

On a side note, what's the point of "don't access model directly from view"?

It makes zero sense when your model is value type.

Especially when you pass view model reference around like cookies in a party so everyone can have it.

How can I use the @Binding Wrapper in the following mvvm architecture?

In your scenario it is more appropriate to have User as-a ObservableObject and pass it by reference between stores, as well as use in corresponding views explicitly as ObservedObject.

Here is simplified demo combined from your code snapshot and applied the idea.

Tested with Xcode 11.4 / iOS 13.4

Sample Image

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let user = User(id: UUID(), firstname: "John")
let rootStore = RootStore(user: user)
let storeA = StoreA(user: user)
let storeB = StoreB(user: user)
rootStore.storeA = storeA
rootStore.storeB = storeB

return ContentView().environmentObject(rootStore)
}
}

public class User: ObservableObject {
public var id: UUID?
@Published public var firstname: String
@Published public var birthday: Date

public init(id: UUID? = nil,
firstname: String,
birthday: Date? = nil) {
self.id = id
self.firstname = firstname
self.birthday = birthday ?? Date()
}
}

public class RootStore: ObservableObject {
@Published var storeA: StoreA?
@Published var storeB: StoreB?

@Published var user: User

init(user: User) {
self.user = user
}
}

public class StoreA: ObservableObject {
@Published var user: User

init(user: User) {
self.user = user
}
}

public class StoreB: ObservableObject {
@Published var user: User

init(user: User) {
self.user = user
}
}

struct ContentView: View {
@EnvironmentObject var rootStore: RootStore

var body: some View {
TabView {
ViewA(user: rootStore.user).environmentObject(rootStore.storeA!).tabItem {
Image(systemName: "location.circle.fill")
Text("ViewA")
}.tag(1)

ViewB(user: rootStore.user).environmentObject(rootStore.storeB!).tabItem {
Image(systemName: "waveform.path.ecg")
Text("ViewB")
}.tag(2)
}
}
}

struct ViewA: View {
@EnvironmentObject var storeA: StoreA // keep only if it is needed in real view

@ObservedObject var user: User
init(user: User) {
self.user = user
}

var body: some View {
VStack {
HStack {
Text("Personal Information")
Image(systemName: "info.circle")
}
TextField("First name", text: $user.firstname)
}
}
}

struct ViewB: View {
@EnvironmentObject var storeB: StoreB

@ObservedObject var user: User
init(user: User) {
self.user = user
}

var body: some View {
Text("\(user.firstname)")
}
}


Related Topics



Leave a reply



Submit