Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cannot use mutating getter on immutable value: 'self' is immutable error

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
like image 645
arakweker Avatar asked Aug 01 '19 20:08

arakweker


2 Answers

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

like image 55
Mojtaba Hosseini Avatar answered Nov 13 '22 13:11

Mojtaba Hosseini


A getter cannot mutate

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 x = Device()
let y = Device()

y.isOn // Doing such will not cause the object to mutate.

Yet the following example, the getter will have a side-effect. The Swift architecture just doesn't allow it.

struct Device2 {
    
    var x = 3
    var isOn: Bool {
        x = 5
        return true
    }
}

let a = Device2()
let b = Device2()

a.isOn // Doing such will mutate the object. a.x will be '5'. While `b.x` will be '3'. Swift doesn't want to allow this.

Lazy is mutating:

struct ContentView: View {
    lazy var x : Double = 3.0

    var body: some View {
       Text("\(x)") // ERROR
    }
}

Will result in the follow ERROR:

Cannot use mutating getter on immutable value: 'self' is immutable

SwiftUI - special case

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, 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

like image 18
mfaani Avatar answered Nov 13 '22 12:11

mfaani