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
.