SwiftedMind
SwiftedMind

Reputation: 4287

How to work with bindings when using a view model vs using @Binding in the view itself?

@State and @Binding work so well in SwiftUI, as long as you put all the view's data inside itself, like this:

struct ColorView: View {
    @Binding public var isBlue: Bool
    
    var body: some View {
        Rectangle()
            .foregroundColor(isBlue ? .blue : .red)
            .onTapGesture {
                self.isBlue.toggle()
        }
    }
}

struct TestView: View {
    @State var isBlue: Bool = false
    
    var body: some View {
        ColorView(isBlue: $isBlue)
    }
}

It works without a problem and it's really simple. But MVVM says you should put all of the view's data inside a view model class, to separate UI from the model. But then you lose @State and @Binding completely. You lose this 2-way binding it seems. Sure, you can do it manually with Combine or something but that should not be the correct way, right?

Whenever I try anything, SwiftUI is really easy and convenient when you don't use view models. Once you put everything inside a view model class, though, everything breaks down and nothing works as convenient anymore. This can't be the case, they have to had thought of that. So I'm missing something here. I'd really appreciate any help. How would you code the above's example using view models (without "hacking" anything manually)? I tried but it doesn't even compile:

struct ColorView: View {
    @ObservedObject var viewModel: ViewModel
    
    class ViewModel: ObservableObject {
        // Binding or Published? Both doesn't seem to work
        @Binding var isBlue: Bool
        
        init(isBlue: Binding<Bool>) { // Or type Bool? But then we lose the binding
            self.$isBlue = isBlue
        }
    }
    
    var body: some View {
        Rectangle()
            .foregroundColor(viewModel.isBlue ? .blue : .red)
            .onTapGesture {
                self.viewModel.isBlue.toggle()
        }
    }
}

struct TestView: View {
    @ObservedObject var viewModel: ViewModel
    
    class ViewModel: ObservableObject {
        @Published var isBlue: Bool = false // We would need a @State here, but then we lose @Published
    }
    
    var body: some View {
        ColorView(viewModel: .init(isBlue: /* ??? */)) // How to pass a binding here`
    }
}

Do I think of this the wrong way?

Upvotes: 16

Views: 10228

Answers (4)

meomeomeo
meomeomeo

Reputation: 942

The short answer is: Yes, You think of this the wrong way.

Why ? @State @StateObject usually hold the source of truth whereas @Binding @ObservedObject @EnvironmentObject ... act as projected to them. If you take source from other view, be kind, don't try to keep it in your @State or @StateObject

So how does your View/ViewModel control the source of truth from other View/ViewModel? Here are some options:

1. The safe way: good old environmentObject. There will be a "god" ObservableObject but hey, as long as it work. If you're inexperienced, just stay with this.

import SwiftUI

@MainActor class TheAppState: ObservableObject {
    @Published private(set) var isBlue = true
    func toggleTheBlue() { isBlue = !isBlue }
}

struct ColorView: View {
    @EnvironmentObject var viewModel: TheAppState
        
    var body: some View {
        Rectangle()
            .foregroundColor(viewModel.isBlue ? .blue : .red)
            .onTapGesture { viewModel.toggleTheBlue() }
    }
    
}


struct TestView: View {
    @StateObject private var viewModel = TheAppState()
    
    var body: some View {
        VStack {
            Button("is\(viewModel.isBlue ? "": " not") blue") { viewModel.toggleTheBlue() }
            ColorView().environmentObject(viewModel)
        }
    }
}

#Preview { TestView() }

Sharing ViewModel is not MVVM ? You may want to debate to this man and it should be on another topic. Technically, in this example, the ColorView is not an independent view but a subview of TestView, so it should be fine here.

2. The improvised way (my recommendation): Utilize closures @Binding @Environment... . This approach allows you to separate your SubViewModel, but it requires adaptation to various scenarios. If you can't separate your View and ViewModel using these tools, your code design are not good enough, you may consider the safe way.

import SwiftUI

struct ColorView: View {
    @StateObject private var viewModel: ViewModel = .init()
    var isBlue: Bool
    var update: (Bool)->()

    var body: some View {
        Rectangle()
            .fill(isBlue ? Color.blue : .red)
            .onTapGesture {
                viewModel.increase()
                update(viewModel.number % 2 == 0)
            }
            .overlay { Text("change to blue if \(viewModel.number) is even") }
            
    }

    class ViewModel: ObservableObject {
        @Published private(set) var number = 0
        func increase() { number += 1 }
    }
}

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

    var body: some View {
        VStack {
            Button("is\(viewModel.isBlue ? "": " not") blue") { viewModel.toggleTheBlue() }
            ColorView(isBlue: viewModel.isBlue) { viewModel.setTheBlue(value: $0) }
        }
    }

    @MainActor class ViewModel: ObservableObject {
        @Published private(set) var isBlue = true
        func toggleTheBlue() { isBlue.toggle() }
        func setTheBlue(value: Bool) { isBlue = value }
    }
}

#Preview { TestView() }

Note that in this example, the ColorViewModel doesn't contain isBlue, and the number isn't completely sync with the color. If it did, it would violate the source of truth principle, which is bad! Always keep in mind: never do two way binding between two source of truth

3. The risky creative: people ignore the source of truth and force their way, using delegates, publishers, notifications, nested viewmodel, onPreferenceChange...

For example, this is my latest idea which using onChange

extension View {
    func linking<T:Equatable>(_ lhs: Binding<T>,_ rhs: Binding<T>) -> some View {
        modifier(BindingBridge(lhs: lhs, rhs: rhs))
    }
}

struct BindingBridge<T: Equatable>: ViewModifier {
    @Binding var lhs: T
    @Binding var rhs: T
    
    func body(content: Content) -> some View {
        content
            .onChange(of: lhs) { if $0 != $1 && $1 != rhs { rhs = $1 } }
            .onChange(of: rhs) { if $0 != $1 && $1 != lhs { lhs = $1 } }
    }
}

to use in OP's example:

struct ColorView: View {
    @Binding var isBlue: Bool
    @StateObject private var viewModel: TheBlueViewModel

    init(isBlue: Binding<Bool>) {
        _isBlue = isBlue
        _viewModel = .init(wrappedValue: .init(isBlue: isBlue.wrappedValue))
    }

    var body: some View {
        Rectangle()
            .foregroundColor(viewModel.isBlue ? .blue : .red)
            .onTapGesture { viewModel.toggle() }
            .linking($isBlue, $viewModel.isBlue)
    }

}

struct TestView: View {
    @StateObject private var viewModel = TheBlueViewModel(isBlue: false)

    var body: some View {
        VStack {
            Button("is\(viewModel.isBlue ? "": " not") blue") { viewModel.toggle() }
            ColorView(isBlue: $viewModel.isBlue)
        }
    }

    @MainActor class ViewModel: ObservableObject {
        @Published var isBlue = true
    }
}

class TheBlueViewModel: ObservableObject {
    @Published var isBlue: Bool

    init(isBlue: Bool) { self.isBlue = isBlue }
    func toggle() { isBlue.toggle() }
}

4. The Complex Way: SwiftUI is not designed for perfect MVVM or any other coding pattern, it's just SwiftUI. So if you want to bend SwiftUI to fit your code pattern safely, it's gonna be complex. As a SwiftUI enthusiast, I do not recommend this approach and will not provide an example here. However, if you are determined to pursue it, you will find the necessary resources eventually.

Upvotes: 1

Muhammad Waqas Bhati
Muhammad Waqas Bhati

Reputation: 2805

Using MVVM is your choice, apple officially does not recommend to use any design pattern. They have given us below concepts and it truly depends on us how we can use them.

1- Property wrappers(@State, @Binding, @Environment)

2- SwiftUI replacement of Storyboards

3- Combine framework provides a declarative Swift API for processing values over time like RxSwift, RxJava, RxKotlin etc.

If you want to use MVVM you will still be using all of the above frameworks

View: Design and bind the view with SwiftUI.

ViewModel: Here you will use @Observable, @Published properties and bind it with View using SwiftUI binding concepts.

Model: Here we are using simple data models.

Network: Here we will be using Combine framework publisher/subscriber mechanism for handling network streams.

NOTE: You can combine all the above concepts and use it in another design pattern like MVP, MVC or VIPER.

Upvotes: 2

Shizam
Shizam

Reputation: 9672

I think what you're looking for is passing @Binding through your ViewModel like so

class ViewModel: ObservableObject {
    @Binding var isBlue:Bool
    
    init(isBlue: Binding<Bool>) {
        _isBlue = isBlue
    }
}

I'm confused on what your example is trying to accomplish so I can't be sure this is what you're after but overall I think you want something like this:

struct ColorView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Rectangle()
            .foregroundColor(viewModel.isBlue ? .blue : .red)
            .onTapGesture {
                self.viewModel.isBlue.toggle()
        }
    }
}

struct TestView: View {
    @ObservedObject var viewModel: ViewModel
    @State var color: Bool = false

    var body: some View {
        ColorView(viewModel: ViewModel(isBlue: $color)) // How to pass a binding here`
    }
}

You're now passing around that @Binding in your ViewModel, any changes made to it will be reflected wherever it is referenced.

Upvotes: -2

Asperi
Asperi

Reputation: 257493

Here is the way (assuming that somewhere you call TestView(viewModel: ViewModel())):

class ViewModel: ObservableObject {
    @Published var isBlue: Bool = false
}

struct TestView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        ColorView(viewModel: self.viewModel)
    }
}

struct ColorView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        Rectangle()
            .foregroundColor(viewModel.isBlue ? .blue : .red)
            .onTapGesture {
                self.viewModel.isBlue.toggle()
        }
    }
}

Upvotes: 2

Related Questions