13

In the tutorial from Ray that I'm following I have the following properties set

struct ContentView : View {

    var rTarget = Double.random(in: 0..<1)
    var gTarget = Double.random(in: 0..<1)
    var bTarget = Double.random(in: 0..<1)
}

These are of course immutable so I can't modify them from a func unless I mark that func as mutating

func reset() {
    rTarget = Double.random(in: 0..<1)
    gTarget = Double.random(in: 0..<1)
    bTarget = Double.random(in: 0..<1)
}

Cannot assign to property: 'self' is immutable

But I call this func from var body

mutating func reset() {
    rTarget = Double.random(in: 0..<1)
    gTarget = Double.random(in: 0..<1)
    bTarget = Double.random(in: 0..<1)
}

fileprivate mutating func displayAlert() -> Alert {
    return Alert(title: Text("Your Score"), message: Text("\(computeScore())"), dismissButton: Alert.Button.destructive(Text("Ok"), onTrigger: {
        self.reset()
    }))
}

 var body: some View {
    Button(action: {
        self.showAlert = true
    }) {
        Text("Hit Me!")
        }.presentation($showAlert) {
            displayAlert()
    }
}

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

But I can't mark var body as mutating var

'mutating' may only be used on 'func' declarations

So at this point, I want to reset the xTarget values each time the user taps on the alert button and I don't know what a good practice will be at this point.

trusk
  • 1,634
  • 2
  • 18
  • 32

4 Answers4

10

I was working on the same article.

I didn't get this problem because I was already using the @State property wrapper. As suggested by kontiki Session 226 (Data Flow Through SwiftUI) is great for understanding how to use which property wrapper when you are updating the data different source.

And If you want to know what is @State this answer has it all.

Here is my code:

@State var rTarget = Double.random(in: 0..<1)
@State var gTarget = Double.random(in: 0..<1)
@State var bTarget = Double.random(in: 0..<1)
@State var rGuess: Double
@State var gGuess: Double
@State var bGuess: Double
Let's_Create
  • 2,963
  • 3
  • 14
  • 33
4

It is hard to tell what is recommended in your case, because in your example you are not showing us what the target variables are actually for.

However, I think it is safe to say that in SwiftUI views, variables that need to change over time, must be either @State or of one of the binding types available to you. Otherwise, it most likely need to be unmutable.

It all comes down to determine what is the "source of truth". If those targets are something internal to the view, then use @State, if the source of truth is external to the view, you would go for one of the bindable options.

I strongly recommend you invest the 37 minutes it takes to watch WWDC2019, Session 226 (Data Flow Through SwiftUI). It will clear all your questions about this.

If you are in a hurry, jump to 5:45. But I do recommend you watch the whole thing. It will save you time eventually.

If you don't know what the "source of truth" is. Then you should definitely must watch the session.

kontiki
  • 37,663
  • 13
  • 111
  • 125
  • And also, if you have some additional time, I would recommend you check WWDC2019, session 415 (Modern Swift API Design). It explains what a property wrapper is, and how it works (i.e., those properties with a *@*, like *@State*, *@Binding*, etc.). You can even create your own. The dollar sign ($) in front of them will start to make sense! This one you can jump to 23:12. – kontiki Jun 23 '19 at 12:24
  • Thanks a lot both of you, I will do this tomorrow. – trusk Jun 23 '19 at 18:44
3

Only solution I found at this point is to also mark the xTarget props as @State and modify them without mutating funcs

@State private var rTarget = Double.random(in: 0..<1)
@State private var gTarget = Double.random(in: 0..<1)
@State private var bTarget = Double.random(in: 0..<1)

But it's not clear to me that it's good practice.

trusk
  • 1,634
  • 2
  • 18
  • 32
  • Yes that is good practice for most scenarios. WWDC2019, Session 226 (Data Flow Through SwiftUI) explains it in more detail. – Daniel Lyons Apr 07 '21 at 23:12
2

This question is old, but it's kinda hard to follow and to understand the answers without more context. The question comes from RayWenderlich - SwiftUI: Getting Started.

You need to do two things in order to reset the game after the alert is presented:

  1. Remove mutating from your function displayAlert()
  2. Add @State in front of the variables you wish to modify (i.e. rTarget, gTarget, bTarget)

Full code for reference - Note that I reset the game using func resetGame()

import SwiftUI

struct ContentView: View {
    // In SwiftUI, when a @State variable changes,
    // the view invalidates its appearance and recomputes the body.
    @State var randomRed = Double.random(in: 0..<1)
    @State var randomGreen = Double.random(in: 0..<1)
    @State var randomBlue = Double.random(in: 0..<1)

    @State var redGuess: Double
    @State var greenGuess: Double
    @State var blueGuess: Double

    @State var showAlert: Bool = false

    func calculateScore() -> String {
        // The diff value is just the distance between two points in three-dimensional space.
        // You subtract it from 1, then scale it to a value out of 100.
        // Smaller diff yields a higher score.
        let redDiff = redGuess - randomRed
        let greenDiff = greenGuess - randomGreen
        let blueDiff = blueGuess - randomBlue
        let diff = sqrt(redDiff * redDiff + greenDiff * greenDiff + blueDiff * blueDiff)

        return "\(Int((1.0 - diff) * 100.0 + 0.5))"
    }

     func resetGame() {
        randomRed = Double.random(in: 0..<1)
        randomGreen = Double.random(in: 0..<1)
        randomBlue = Double.random(in: 0..<1)

        redGuess = 0.5
        greenGuess = 0.5
        blueGuess = 0.5
    }

    var body: some View {
        VStack {
            HStack {
                VStack {
                    Color(red: randomRed, green: randomGreen, blue: randomBlue)
                    Text("Match this color")
                }
                VStack {
                    Color(red: redGuess, green: greenGuess, blue: blueGuess)
                    Text("R: \(Int(redGuess * 255))  G: \(Int(greenGuess * 255))  B: \(Int(blueGuess * 255))")
                }
            }

            Button(action: {self.showAlert = true} ) {
                Text("Hit me")
            }.alert(isPresented: $showAlert, content: {
                Alert(title: Text("Your Score"), message: Text(self.calculateScore()),
                      dismissButton: Alert.Button.default(Text("OK"), action: { self.resetGame()
                }))
            }).padding()

            ColorSlider(value: $redGuess, textColor: .red)
            ColorSlider(value: $greenGuess, textColor: .green)
            ColorSlider(value: $blueGuess, textColor: .blue)
        }
    }
}

struct ColorSlider: View {
    // Use @Binding instead of @State, because the ColorSlider view
    // doesn't own this data—it receives an initial value from its parent view and mutates it.
    @Binding var value: Double
    var textColor: Color

    var body: some View {
        HStack {
            Text("0").foregroundColor(textColor)
            Slider(value: $value)
            Text("255").foregroundColor(textColor)
        }.padding(.horizontal)  // Add some space before & after the text
    }
}
DoesData
  • 6,594
  • 3
  • 39
  • 62