26

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
mfaani
  • 33,269
  • 19
  • 164
  • 293
arakweker
  • 1,535
  • 4
  • 18
  • 40
  • 3
    A workaround solution I found to this problem is to use computed properties instead of lazy vars. I don't understand why I can't use lazy vars however, neither do I understand the proposed solution – charelf Nov 09 '19 at 14:10
  • 1
    @charel-f I wrote a new answer. Please take a look – mfaani Feb 11 '20 at 17:32
  • Thanks, your new answer is very concise and easy to understand – charelf Feb 13 '20 at 18:11

2 Answers2

24

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

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
23

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 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".

Exceptions

The Swift architecture has two exceptions to this rule:

  • use lazy
  • use @State or some other property wrapper

Lazy is mutating:

struct Device2 {
    lazy var x : Double = 3.0

    func log() {
        print(x)
    } 
}

Even thought print(x) will mutate x upon getting the value of x, it's fine 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...

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

mfaani
  • 33,269
  • 19
  • 164
  • 293
  • 33
    This helped me realize that a lazy property mutates the object on the first invocation and so will always be considered as `mutating`. It's unfortunate but it at least makes sense now. – Sebastien Martin Apr 22 '20 at 12:25
  • @SebastienMartin lol. I generated the error without using lazy properties. Then I noticed your comment and learned that lazy properties are implicitly `mutating` – mfaani Apr 27 '22 at 18:07
  • "Doing such will mutate the object. a.x will be '5'. While `b.x` will be '3'. Swift doesn't want to allow this." This explanation is wrong, `a` and `b` can have different values of `x`. I do not know why you brought this into subject anyway. What Swift does not like is this part `var isOn: Bool { x = 5 return true }`. It is about computed variable not `a` or `b` having different values of `x`. For the same reason we are not allowed to touch lazy from computed variable because it can change another variable inside computation closure. This is the explanation – Farid Jan 09 '23 at 21:10
  • @Farid lol. Not sure how that has been able to go unnoticed till you mentioned. Thank you pointing that out. Does it look ok now? – mfaani Jan 09 '23 at 22:01
  • @mfaani Yes, looks perfect. Just one thing lazy is not an exception it is still part of the "A getter cannot mutate" error. Made an edit – Farid Jan 10 '23 at 20:38
  • @Farid The way I see it is: `var isOn: Bool { x = 5; return true }` doesn't allow it to compile. Why? Because you can't mutate upon getting. However `lazy var x : Double = 3.0; print(x)` will mutate (in other words _set_) the value of `x` upon getting its value. And it compiles fine. This though was an exception to the Swift architecture rule I mentioned. Can you rollback your edit? – mfaani Jan 10 '23 at 21:35
  • This will not compile because lazy is mutating. `struct ContentView: View { lazy var x : Double = 3.0 var body: some View { Text("\(x)") // ERROR } }` Am I missing something on your reply? – Farid Jan 10 '23 at 21:44
  • @Farid. I believe you're missing.. What you're talking about is something **else**. Instead let's focus on this example: `struct Foo { lazy var x : Double = 3.0 } ; print(x)` (Nothing is SwiftUI). When you do `print(x)` are you mutating the `Foo` instance? You'd say yes. Next question: was the value of `x` set before doing `pinrt(x)`? You's day No. But didn't we say "Swift has an architecture that doesn't allow "getters to mutate an object"? How does this happen then? My answer: `lazy` is an exception. It allows a getter to set. Would you mind rolling back the edit if everything is ok? – mfaani Jan 10 '23 at 23:04
  • @mfaani Yeap, I got it and rolled it back. I do not know if people will still be confused with that example or not. I would suggest we change the code for **Lazy is mutating** to this `struct Device2 { var x = 3 lazy var isOn: Bool = { x = 5 return true }() } let a = Device2() let b = a print(a.isOn)` – Farid Jan 11 '23 at 14:08
  • @Farid I made another edit. I humbly prefer it the way it is now. Folks can read your comments here. or you if you like, maybe you can write another answer... – mfaani Jan 11 '23 at 14:21