I have the following Swift view:
struct GameView: View {
@State private var userWon: Bool = false {
didSet {
print("userWon: \(userWon)") // not called
// TODO: Change game state here
}
}
var body: some View {
ZStack {
VStack {
GameViewRepresentable(userWon: $userWon)
.onChange(of: userWon) { newValue in
print("GameViewRepresentable: \(newValue)") // called
// TODO: or change game state here?
}
}
.clipped()
}
}
}
The view representable is like so:
struct GameViewRepresentable: UIViewRepresentable {
typealias UIViewType = GameView
@Binding var userWon: Bool {
willSet { newValue
print("willSet userWon: \(newValue)")
}
didSet {
print("didSet userWon: \(userWon)")
}
}
func makeUIView(context: Context) -> GameView {
let gameView = GameView()
gameView.delegate = context.coordinator
return gameView
}
func updateUIView(_ uiView: GameView, context: Context) {
}
func makeCoordinator() -> GameViewCoordinator {
GameViewCoordinator(self)
}
class GameViewCoordinator: GameViewDelegate {
var parent: GameViewRepresentable
init(_ parent: GameViewRepresentable) {
self.parent = parent
}
func userWon() {
self.parent.userWon = true
}
}
}
I can see that the userWon method is correctly getting called inside the coordinator which correspondingly calls willSet and didSet in GameViewRepresentable, but the didSet in the GameView for the binding userWon property is never called. However the onChange in GameView does get called. So is that the right way to respond to changes to the state property? I was expecting the didSet inside the GameView to get called instead.
Yes,
onChangeis how you detect@Statechanges.It is perfectly normal for
willSetanddidSetto not be triggered on a@Stateor@Binding. Swift inserts calls towillSetanddidSetin the setter of theuserWonproperty, so ifuserWonchanges not by callinguserWon's setter, thenwillSetanddidSetwill not be called.Your
userWonproperty lowers to something like this:Consider this really simple example:
No matter how many times I toggle the toggle,
willSetanddidSetare not called. This is because theToggledoesn't actually change the value ofuserWonby literally saying e.g.userWon = true(which would have called its setter).Toggledoesn't know thatuserWonexists at all.What actually happens is you passed a
Binding<Bool>to theToggle, which was theprojectedValueof theState.$userWonis just a short way of saying_userWon.projectedValue. TheTogglewill at some point set thewrappedValueproperty of theBinding<Bool>. This will then cause thewrappedValueof theStateto be set, and the view gets updated.Notice that everything is done through the
wrappedValueproperties ofStateandBinding, not youruserWonproperty.