I feel like I\'m missing something very basic, but this example SwiftUI code will not modify the view (despite the Binding updating) when the button is clicked
Tutorials
In the top Level of SwiftUI, @Binding cannot refresh View hierarchy unless manually adding a @state or other refreshing triggers.
struct ContentView: View {
@Binding var isSelected : Bool
@State var hiddenTrigger = false
var body: some View {
VStack {
Text("\(hiddenTrigger ? "" : "")")
Button(action: {
self.isSelected.toggle()
self.hiddenTrigger = self.isSelected
}) {
Text(self.isSelected? "Selected" : "not Selected")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var selected: Bool = false
static var previews: some View {
ContentView(isSelected: Binding<Bool>(get: {selected}, set: { newValue in
selected = newValue}))
}
}
SwiftUI View affects @Binding
. @State
affects SwiftUI View.
@State
var affects the view, but to affect another @State
it must be used as binding by adding leading $
to value name and it works only inside SwiftUI.
To trigger SwiftUI change from outside, i.e. to deliver/update Image, use Publisher that looks like this:
// Declare publisher in Swift (outside SwiftUI)
public let imagePublisher = PassthroughSubject<Image, Never>()
// And within SwiftUI it must be handled:
struct ContentView: View {
// declare @State that updates View:
@State var image: Image = Image(systemName: "photo")
var body: some View {
// Use @State image declaration
image
// Subscribe this value to publisher "imagePublisher"
.onReceive(imagePublisher, perform: { (output: Image) in
// Whenever publisher sends new value, old one to be replaced
self.image = output
})
}
}
// And this is how to send value to update SwiftUI from Swift:
imagePublisher.send(Image(systemName: "photo"))
You need to use @State instead of @Binding.
If the UI should update when its value changes, you designate a variable as a @State variable. It is the source of truth.
You use @Binding instead of @State, when the view doesn't own this data and its not the source of truth.
Here is your variable:
@State var isSelected: Bool
You have not misunderstood anything. A View using a @Binding will update when the underlying @State change, but the @State must be defined within the view hierarchy. (Else you could bind to a publisher)
Below, I have changed the name of your ContentView to OriginalContentView and then I have defined the @State in the new ContentView that contains your original content view.
import SwiftUI
struct OriginalContentView: View {
@Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView: View {
@State private var selected = false
var body: some View {
OriginalContentView(isSelected: $selected)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Looking into this some more I think I understand what's happening.
In this instance I want to use @Binding
as I'm building a custom control (like SwiftUI's native Toggle
, which also binds to a Bool
)
The issue is that the static state in ContentView_Previews
(i.e., the line @State static var selected: Bool = false
) does not trigger a re-render of the preview when the state changes, so even though the selected state has changed due to interaction with the control, the control (a child of ContentView_Previews
) does not re-render itself
This makes it tough to test controls in isolation in the SwiftUI preview, however moving the state into a dummy ObservableObject
instance functions correctly. Here's the code:
import SwiftUI
import Combine
class SomeData: ObservableObject {
@Published var isOn: Bool = false
}
struct MyButton: View {
@Binding var isSelected: Bool
var body: some View {
Button(action: {
self.isSelected.toggle()
}) {
Text(isSelected ? "Selected" : "Not Selected")
}
}
}
struct ContentView: View {
@EnvironmentObject var data: SomeData
var body: some View {
MyButton(isSelected: $data.isOn)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(SomeData())
}
}
It seems that a change in @State static var
doesn't trigger a preview re-render. In the above code my @Binding
example is moved into MyButton
and the content view's dummy environment instance is bounds to its isSelected
property. Tapping the button updates the view as expected in the SwiftUI preview.