2

I have a view with a separate view model for it's logic. In my view model I've created a binding and I want the Toggle in my View to reflect and act on this. But it the moment, the view reads the initial state of my binding, but it does not allow me to update it (I can't move the toggle box). And I have no idea why this is.

This is my View (simplified):

public var viewModel: MyViewModel // passed as a dependency to my view
var body: some View {
    Toggle("Test", isOn: viewModel.isOn)
}

This is the code in my ViewModel:

public var isOn: Binding<Bool> = .constant(false) {
    didSet {
        print("Binding in practice!")
    }
}

As I said, the code runs, and the initial value for isOn is respected: if I set it to .constant(true) or .constant(false) the checkbox is in the correct state. But I am not allowed to change the checkbox. Also the print()-statement is never executed. So it seems to me there are 2 problems:

  • I cannot change the state for isOn
  • The didSet is not executed

Can anyone point me in the right direction?

Edit:

So, actually some of you pointed my in the direction I've already tried and that did not work. In the above writing I simplified my use case, but it's actually a bit more abstract than that: I've got a protocol for my View Model that I use as a dependency. What I've got working now (with binding and observing) is the following:

ViewModelProtocol + ViewModel:

public protocol ViewModelProtocol: ObservableObject {
    var isOn: Binding<Bool> { get }
}

public class ViewModel: ViewModelProtocol {
    private var _isOn: Bool = false
    public var isOn: Binding<Bool> {
        Binding<Bool>(
            get: { self._isOn },
            set: {
                self._isOn = $0
                // custom code
            }
        )
    }

    // More code that has @Published properties
}

View:

struct MyView<Model>: View where Model: ViewModelProtocol {
    @ObservedObject var viewModel: Model

    var body: some View {
        Toggle("Test", isOn: viewModel.isOn)
        
        // More code that uses @Published properties
    }
}

Once again, I simplified the example and stripped out all clutter, but with this setup I am able to do what I want. However, I'm still not sure if this is the correct way to do this.

The implementation of making my view generic is based on https://stackoverflow.com/a/59504489/1471590

Giel Berkers
  • 2,852
  • 3
  • 39
  • 58

2 Answers2

4

If you want to drive your view from the properties of the view model, then make it an ObservableObject

class ViewModel: ObservableObject {
    @Published var isOn = false {
        didSet {
            print("Binding in practice")
        }
    }
}

And in your view, you can bind the toggle to this value:

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

    var body: some View {
        Toggle("Test", isOn: $viewModel.isOn)
    }
}
Abizern
  • 146,289
  • 39
  • 203
  • 257
  • Thanks, but I simplified my example. The actual use case is that I pass viewModel as a dependency to my View and don't instantiate it in the view. Secondly, the dependency is actually a protocol, so I could use a property wrapper for it. I'll update my example to clarify it a bit more (and to test if my own found solution is actually the right one). – Giel Berkers Jul 26 '21 at 12:29
0

After some more tinkering and the response of @RajaKishan I came up with the following solution that works:

private var _isOn: Bool = false
public var isOn: Binding<Bool> {
    Binding<Bool>(
        get: { self._isOn },
        set: {
            self._isOn = $0
            print("Room for custom logic")
        }
    )
}
Giel Berkers
  • 2,852
  • 3
  • 39
  • 58