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, onChange
is how you detect @State
changes.
It is perfectly normal for willSet
and didSet
to not be triggered on a @State
or @Binding
. Swift inserts calls to willSet
and didSet
in the setter of the userWon
property, so if userWon
changes not by calling userWon
's setter, then willSet
and didSet
will not be called.
Your userWon
property lowers to something like this:
private var _userWon = State(initialValue: false)
private var userWon: Bool {
get { _userWon.wrappedValue }
set {
// <willSet is called here>
_userWon.wrappedValue = newValue
// <didSet is called here>
}
}
private var $userWon: Binding<Bool> {
_userWon.projectedValue
}
Consider this really simple example:
@State private var userWon: Bool = false {
willSet {
print("willSet")
}
didSet {
print("didSet")
}
}
var body: some View {
Toggle(isOn: $userWon) {
Text("Foo")
}
}
No matter how many times I toggle the toggle, willSet
and didSet
are not called. This is because the Toggle
doesn't actually change the value of userWon
by literally saying e.g. userWon = true
(which would have called its setter). Toggle
doesn't know that userWon
exists at all.
What actually happens is you passed a Binding<Bool>
to the Toggle
, which was the projectedValue
of the State
. $userWon
is just a short way of saying _userWon.projectedValue
. The Toggle
will at some point set the wrappedValue
property of the Binding<Bool>
. This will then cause the wrappedValue
of the State
to be set, and the view gets updated.
Notice that everything is done through the wrappedValue
properties of State
and Binding
, not your userWon
property.