Why I Can Mutate @State var? How Does @State Property Wrapper Work Inside?

Mutating regular member var get error:

"Cannot assign to property: 'self' is immutable"
"Cannot use mutating member on immutable value: 'self' is immutable"

struct porque: View {
    @State private var flag = false
    private var anotherFlag = false

    mutating func changeMe(_ value: Bool) {
        self.anotherFlag = value
    }

    var body: some View {
        Button(action: {
            // This is okay
            self.flag = true
            // Cannot assign to property: 'self' is immutable
            self.anotherFlag = true
            // Cannot use mutating member on immutable value: 'self' is immutable
            changeMe(true)
        }) {
            Text("Hi")
        }
    }
}

Why I can mutate @State var, but not regular member var?

1 Like

There are (at least) two questions here: why can we mutate flag, and why can't we mutate anotherFlag? Let's start with why we can't mutate anotherFlag.

Here's a simpler example of the immutable property error:

struct SimpleStruct {
    var anotherFlag: Bool {
        _anotherFlag = true
//      ^~~~~~~~~~~~
//      error: cannot assign to property: 'self' is immutable
        return _anotherFlag
    }

    private var _anotherFlag = false
}

Usually, inside a property getter, self is immutable, and usually, we can't assign to a property of an immutable value. Hence the error.

But I said “usually” twice, and there is a way around each of those “usually”s.

The first usually was that, inside a property getter, self is immutable. We can change self to be mutable by declaring the getter mutating:

struct SimpleStruct {
    var anotherFlag: Bool {
        mutating get {
            _anotherFlag = true
            return _anotherFlag
        }
    }

    private var _anotherFlag = false
}

Now self is mutable inside the getter, so Swift allows us to assign to its mutable properties. But this has two problems. The first is that we can only use this getter on mutable values:

var s0 = SimpleStruct()
_ = s0.anotherFlag // ok, and modifies s0

let s1 = SimpleStruct()
_ = s1.anotherFlag
//  ^~ error: cannot use mutating getter on immutable value: 's1' is a 'let' constant

The second is that SwiftUI doesn't allow us to declare body with a mutating get:

struct SimpleView: View {
//     ^ error: type 'SimpleView' does not conform to protocol 'View' 
    var body: some View {
        mutating get { Text("Hello") }
    }
}

The View protocol requires that body's getter be non-mutating.

The second “usually” was that we can't assign to a property of an immutable value. We can make a property assignable by declaring the setter nonmutating:

struct SimpleStruct {
    var anotherFlag: Bool {
        get {
            _anotherFlag = true
            return _anotherFlag
        }
    }

    private var _anotherFlag: Bool {
        get { return storage.pointee }
        nonmutating set { storage.pointee = newValue }
    }

    private let storage = UnsafeMutablePointer<Bool>.allocate(capacity: 1)
}

(Note that this example leaks memory. It's just an example.)

This works, but note that it acts like a reference type:

let s0 = SimpleStruct()
let s1 = s0
_ = s1.get // effectively mutates both s0 and s1

SwiftUI's State works using nonmutating set.

Specifically, State is a property wrapper and its wrappedValue property has a nonmutating set. So when you declare flag like this:

@State private var flag = false

Swift translates that into three properties:

private var _flag: State<Bool> = State(initialValue: false)
private var $flag: Binding<Bool> { return _flag.projectedValue }
private var flag: Bool {
    get { return _flag.wrappedValue }
    nonmutating set { _flag.wrappedValue = newValue }
}

The flag property here has a nonmutating set because State's wrappedValue has a nonmutating set.

Under the hood, SwiftUI allocates storage for the current value of your flag property, and sets an internal property of the State wrapper to identify that storage. You can get a glimpse of the inner workings by calling dump(self._flag). Example:

struct ExampleView: View {
    @State var flag = false

    init() {
        dump(self._flag)
    }

    var body: some View {
        Button("flag = \(flag)" as String, action: { self.flag = true; dump(self._flag) })
    }
}

The call to dump in init prints this:

▿ SwiftUI.State<Swift.Bool>
  - _value: false
  - _location: nil

The internal _location property identifies where SwiftUI privately stores the current value of the property. It gets initialized after you pass the view to SwiftUI. When you click the button, the dump in the action closure prints this:

▿ SwiftUI.State<Swift.Bool>
  - _value: false
  ▿ _location: Optional(SwiftUI.StoredLocation<Swift.Bool>)
    ▿ some: SwiftUI.StoredLocation<Swift.Bool> #0
      - super: SwiftUI.AnyLocation<Swift.Bool>
        - super: SwiftUI.AnyLocationBase
      ▿ viewGraph: Optional(SwiftUI.ViewGraph)
        - some: SwiftUI.ViewGraph #1
      ▿ signal: AttributeGraph.WeakAttribute<()>
        - _subgraph: <AGSubgraph 0x60000353c840 [0x7fff805cbf90]> #2
          - super: NSObject
        ▿ _attribute: AttributeGraph.Attribute<()>
          ▿ identifier: __C.AGAttribute
            - id: 36
      ▿ valueLock: SwiftUI.SpinLock
        ▿ lock: __C.os_unfair_lock_s
          - _os_unfair_lock_opaque: 0
      - value: true
      ▿ savedValues: 1 element
        - false
      - _wasRead: true
      ▿ cache: SwiftUI.AtomicVariable<SwiftUI.LocationProjectionCache>
        ▿ _value: SwiftUI.LocationProjectionCache
          - cache: 0 key/value pairs
        ▿ lock: SwiftUI.SpinLock
          ▿ lock: __C.os_unfair_lock_s
            - _os_unfair_lock_opaque: 0

Note here that, even after assigning self.flag = true, _flag._value is still false. That _value property only stores the initial value of the property; it can't be mutated inside body (because self is immutable inside body's getter, because body's getter is not declared mutating). Instead, the current value is found in _flag._location!.value.

8 Likes

Oh wow! Thank you for such deep explanation!

3 Likes
Terms of Service

Privacy Policy

Cookie Policy