Search code examples
swiftuibindingviewmodelswiftui-environmentobservation

SwiftUI: How to create a binding to a property of an environment object?


The following minimal code toggles the display of details by pressing a button.

struct ContentView: View {
    @State var showDetails: Bool = false
    var body: some View {
        VStack {
            DetailsButton(showDetails: $showDetails)  // 1
            if showDetails {
                Text("This is my message!")
            }
        }
    }
}

struct DetailsButton: View {
    @Binding var showDetails: Bool
    var body: some View {
        Button("\(showDetails ? "Hide" : "Show") Details") {
            showDetails.toggle()
        }
    }
}

I would like to achieve the same thing, but moving the showDetails property into a ViewModel which I pass as an environment variable:

(Note that I'm using the new Observation framework of Swift 5.9 and iOS 17 / macOS 14.)

@Observable class ViewModel {
    var showDetails: Bool
}

Ordinarily I would initialize it in the App class:

struct TestApp: App {
    @State var vm = ViewModel()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(vm)
        }
    }
}

and passing the ViewModel into ContentView as an environment variable:

@Environment(ViewModel.self) private var vm

In this scenario, how can I pass the binding for vm.showDetails from ContentView to DetailsButton as I did in the original example? (See the line marked "1")


Solution

  • To use your vm.showDetails in DetailsButton try this approach, using a @Bindable

     import SwiftUI
     import Observation
     
     @main
     struct TestApp: App {
         @State private var vm = ViewModel()
         
         var body: some Scene {
             WindowGroup {
                 ContentView()
                     .environment(vm)
             }
         }
     }
      
     
    @Observable class ViewModel {
        var showDetails: Bool = false // <--- here
    }
    
    struct ContentView: View {
        @Environment(ViewModel.self) var vm  
        
        var body: some View {
            @Bindable var vm = vm  // <--- here
            VStack {
                DetailsButton(showDetails: $vm.showDetails) 
                if vm.showDetails {   // <--- here
                    Text("This is my message!")
                }
            }
        }
    }
    
    struct DetailsButton: View {
        @Binding var showDetails: Bool  
        
        var body: some View {
            Button("\(showDetails ? "Hide" : "Show") Details") {  
                showDetails.toggle()  
            }
        }
    }