Reputation: 1635
I'm trying to reuse an older piece of Swift code, but getting an error 'Cannot use mutating getter on immutable value: 'self' is immutable error'. Xcode wanted to add 'mutating' before the func, and offered to do so through a 'fix'. So the error is gone there but still remains at the 'Text' statements.
import SwiftUI
struct ContentView: View {
typealias PointTuple = (day: Double, mW: Double)
let points: [PointTuple] = [(0.0, 31.98), (1.0, 31.89), (2.0, 31.77), (4.0, 31.58), (6.0, 31.46)]
lazy var meanDays = points.reduce(0) { $0 + $1.0 } / Double(points.count)
lazy var meanMW = points.reduce(0) { $0 + $1.1 } / Double(points.count)
lazy var a = points.reduce(0) { $0 + ($1.day - meanDays) * ($1.mW - meanMW) }
lazy var b = points.reduce(0) { $0 + pow($1.day - meanDays, 2) }
lazy var m = a / b
lazy var c = meanMW - m * meanDays
lazy var x : Double = bG(day: 3.0)
lazy var y : Double = bG(day: 5.0)
lazy var z : Double = bG(day: 7.0)
mutating func bG(day: Double) -> Double {
return m * day + c
}
var body: some View {
VStack {
Text("\(x)")
Text("\(y)")
Text("\(z)")
}
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
Upvotes: 30
Views: 26587
Reputation: 36427
This is mainly a design Swift is enforcing with its getters. The principle is:
The getters should not mutate the object. Because developers may not be expecting that. They should only expect a change when you're using the setter or calling a mutating function. A getter is neither of them.
The following example works as expected:
struct Device {
var isOn = true
}
let a = Device()
let b = a
print(a.isOn)
When we print, we get the value of a.isOn
. Both a,b are identical.
Yet in the following example, the getter will have a side-effect. It won't even compile. But let's just assume that it did and see what happens.
struct Device2 {
var x = 3
var isOn: Bool {
x = 5
return true
}
}
let a = Device2()
let b = a
print(a.isOn)
When we print, we get the value of a.isOn
. However this time the a,b are no longer identical. a.x
will now be 5, because a copy-on-write would have happened, while b.x
would still be 3.
Swift has an architecture that doesn't allow "getters to mutate an object".
The Swift architecture has two exceptions to this rule:
lazy
@State
or some other property wrapper managed by SwiftUI that can be written to and manages state.struct Device2 {
lazy var x : Double = 3.0
func log() { // ‼️ ERROR - Cannot use mutating getter on immutable value: 'self' is immutable | Mark method 'mutating' to make 'self' mutable
print(x)
}
}
To make it compile you have to add mutating
.
print(x)
can then mutate Device2 without problems, because x
is a lazy
property.
You might be wondering what's different between lazy var x = 5
and var x = 5
and the answer is that the latter has the value of x
set upon initialization of the struct...
Because the body
variable is a computed property, you can't mutate/set variables. There's a way around that though.
Mark the variable with a @State
property wrapper.
Example. The following code won't compile:
struct ContentView: View {
var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(currentDate)")
.onReceive(timer) { input in
currentDate = input // ERROR: Cannot assign to property: 'self' is immutable
}
}
}
Yet the following will compile, just because it has @State
struct ContentView: View {
@State var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Text("\(currentDate)")
.onReceive(timer) { input in
currentDate = input
}
}
}
For more on that see here
Upvotes: 27
Reputation: 12294
Here is the proof that lazy DOES trigger a mutation in the mind of the Swift compiler chain:
As @MojtabaHosseini says, "Lazy property values will be associated on the first call and this mutates the overall object. So they are considered as mutating."
Upvotes: 1
Reputation: 120113
Because when you call x
inside the struct, it's not clear whether the contentView
itself is mutable or not. A value type gets mutable only when it is defined as a var
.
So it would help if you wrapped it with an immutable value before using it inside a builder function inside the struct.
like this:
func xValue() -> Double {
var mutatableSelf = self
return mutatableSelf.x
}
var body: some View {
VStack {
Text("\(xValue())")
}
}
💡Note: Lazy
property's value will be associated on the first call and this mutates the object. So they are considered as mutating
Upvotes: 28