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

1

5

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")

Desberg answered 17/9, 2023 at 21:43 Comment(0)
C
10

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()  
        }
    }
}
Croat answered 17/9, 2023 at 23:22 Comment(2)
Thank you!! I didn't know about Bindable. It seems that this is precisely what it's meant for. I'll try it out in my more complex case to see if there are any snags, but I'm guessing that this solves it!! ๐Ÿ‡บ๐Ÿ‡ฆ๐Ÿ’ช๐Ÿ‡บ๐Ÿ‡ฆ โ€“ Desberg
You can find more info on @Bindable at Model data (bottom of the page). If my answer helped, consider accepting it by ticking the tick mark next to it. โ€“ Croat

© 2022 - 2024 โ€” McMap. All rights reserved.