'Binding value from an ObservableObject

Aim:

I have a model which is an ObservableObject. It has a Bool property, I would like to use this Bool property to initialise a @Binding variable.

Questions:

  1. How to convert an @ObservableObject to a @Binding ?
  2. Is creating a @State the only way to initialise a @Binding ?

Note:

  • I do understand I can make use of @ObservedObject / @EnvironmentObject, and I see it's usefulness, but I am not sure a simple button needs to have access to the entire model.
  • Or is my understanding incorrect ?

Code:

import SwiftUI
import Combine
import SwiftUI
import PlaygroundSupport

class Car : ObservableObject {

    @Published var isReadyForSale = true
}

struct SaleButton : View {

    @Binding var isOn : Bool

    var body: some View {

        Button(action: {

            self.isOn.toggle()
        }) {
            Text(isOn ? "On" : "Off")
        }
    }
}

let car = Car()

//How to convert an ObservableObject to a Binding
//Is creating an ObservedObject or EnvironmentObject the only way to handle a Observable Object ?

let button = SaleButton(isOn: car.isReadyForSale) //Throws a compilation error and rightly so, but how to pass it as a Binding variable ?

PlaygroundPage.current.setLiveView(button)


Solution 1:[1]

Binding variables can be created in the following ways:

  1. @State variable's projected value provides a Binding<Value>
  2. @ObservedObject variable's projected value provides a wrapper from which you can get the Binding<Subject> for all of it's properties
  3. Point 2 applies to @EnvironmentObject as well.
  4. You can create a Binding variable by passing closures for getter and setter as shown below:
let button = SaleButton(isOn: .init(get: { car.isReadyForSale },
                                    set: { car.isReadyForSale = $0} ))

Note:

  • As @nayem has pointed out you need @State / @ObservedObject / @EnvironmentObject / @StateObject (added in SwiftUI 2.0) in the view for SwiftUI to detect changes automatically.
  • Projected values can be accessed conveniently by using $ prefix.

backup

Solution 2:[2]

  1. You have several options to observe the ObservableObject. If you want to be in sync with the state of the object, it's inevitable to observe the state of the stateful object. From the options, the most commons are:

    • @State
    • @ObservedObject
    • @EnvironmentObject

It is upto you, which one suits your use case.

  1. No. But you need to have an object which can be observed of any change made to that object in any point in time.

In reality, you will have something like this:

class Car: ObservableObject {
    @Published var isReadyForSale = true
}

struct ContentView: View {

    // It's upto you whether you want to have other type 
    // such as @State or @ObservedObject
    @EnvironmentObject var car: Car

    var body: some View {
        SaleButton(isOn: $car.isReadyForSale)
    }

}

struct SaleButton: View {
    @Binding var isOn: Bool
    var body: some View {
        Button(action: {
            self.isOn.toggle()
        }) {
            Text(isOn ? "Off" : "On")
        }
    }
}

If you are ready for the @EnvironmentObject you will initialize your view with:

let contentView = ContentView().environmentObject(Car())

Solution 3:[3]

struct ContentView: View {
    @EnvironmentObject var car: Car

    var body: some View {
        SaleButton(isOn: self.$car.isReadyForSale)
    }
}

class Car: ObservableObject {
    @Published var isReadyForSale = true
}

struct SaleButton: View {
    @Binding var isOn: Bool

    var body: some View {
        Button(action: {
            self.isOn.toggle()
        }) {
            Text(isOn ? "On" : "Off")
        }
    }
}

Ensure you have the following in your SceneDelegate:

// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
    .environmentObject(Car())

Solution 4:[4]

In my case i used .constant(viewModel) to pass viewModel to ListView @Binding var viewModel

Example

struct CoursesView: View {
    
    @StateObject var viewModel = CoursesViewModel()
    
    var body: some View {
        
        ZStack {
            ListView(viewModel: .constant(viewModel))
            ProgressView().opacity(viewModel.isShowing)
        }
    }
}

struct ListView: View {
    
    @Binding var viewModel: CoursesViewModel
    
    var body: some View {
        
        List {
            ForEach(viewModel.courses, id: \.id) { course in
                Text(couse.imageUrl)
            }
        }
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3
Solution 4 Ghanshyam Doifode