Swift language and swift property wrapper, what's the use of `nonmutating set`?

78 Views Asked by At

In SwiftUI, @State is defined as:

@frozen @propertyWrapper public struct State<Value> : DynamicProperty {

And it has a computed property:

    public var wrappedValue: Value { get nonmutating set }

Question:

  1. why doesn't wrappedValue need to implement the getter and setter? what is this shorthand syntax?
  2. what's the intention of nonmutating set? is it trying to disable the setter?
  3. questions around the nonmutating set is sometimes mutating in the following snippet:
struct PlayButtonStackOverflow: View {
    @State private var isPlaying: Bool = false
    var body: some View {
        // TODO: 3.3: this does NOT change the state. why?
        $isPlaying.wrappedValue = true
        print($isPlaying.wrappedValue.description) // prints false
        
        return Button(isPlaying ? "isPlaying = true" : "isPlaying = false") {
        // TODO: 3.1: this does change the state.
            $isPlaying.wrappedValue = true
            // TODO: 3.1: and this prints true, why?
            print($isPlaying.wrappedValue )
            // `$isPlaying.wrappedValue` is accessing the State.projectValue, which is a Binding, and the Binding.wrappedValue has a non mutating setter. how come setter is actually mutating?

            // TODO: 3.2: Compile error: Referencing property 'wrappedValue' requires wrapper 'Binding<Bool>'
            // why? is there a way to access the State.wrappedValue setter? How is it being disabled?
            // isPlaying.wrappedValue = true
        }.padding()
    }
}

Update A:

Line at TODO 3.3 gives Modifying state during view update, this will cause undefined behavior. That explains some part of it. Since a state change triggers the body to evaluate again. But in this case, all mutation is in one direction.

Question A.1: why is TODO 3.3 still prints false?

Question A.2: trying to wrap my head around this, is $isPlaying.wrappedValue = true and isPlaying = true having the same effect at line TODO 3.3?

Question A.3: since Binding is a struct, passing a value type around create copies of it. But in this case passing around $isPlaying to another func can mutate some value at call site? why?

1

There are 1 best solutions below

0
malhal On
  1. thats the public declaration, the implementation is private and hidden.
  2. it's because the setter isn't changing the value of that struct, the setter is just forwarding the call on to an internal object.
  3. You need to call the isPlaying state getter somewhere in body for the set to be detected and body to be called again. SwiftUI tracks these dependencies. let _ = isPlaying at the top of body would do it.

It might help to mention the $ syntax is sugar for Binding(get: { }, set: { }) i.e. a pair of 2 closures. Declaring $isPlaying, i.e. Binding(get: { self.isPlaying }, set { self.isPlaying - newValue}) does not call the @State var isPlaying getter immediately so is not tracked as a dependency on body.

Property wrappers also have an mutating func update(), which SwiftUI calls before calling body, docs say "SwiftUI calls this function before rendering a view’s body to ensure the view has the most recent value.". It's possible that this gets the value back out of the internal object and sets it on this struct.