Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI call mutating func from var body

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.

like image 479
trusk Avatar asked Jun 23 '19 06:06

trusk


People also ask

What is a mutating function in Swift?

Essentially, a function that’s been marked as mutating can change any property within its enclosing value. The word “value” is really key here, since Swift’s concept of structured mutations only applies to value types, not to reference types like classes and actors.

How to modify the properties of a value type in Swift?

In swift, classes are reference type whereas structures and enumerations are value types. The properties of value types cannot be modified within its instance methods by default. In order to modify the properties of a value type, you have to use the mutating keyword in the instance method.

Can a mutating function be a part of a protocol?

Although the concept of separating mutating and non-mutating APIs is something that’s unique to value types, we can still make a mutating function a part of a protocol as well — even if that protocol might end up being adopted by a reference type, such as a class.

How can Swift help us write more robust code?

One of the ways in which Swift helps us write more robust code is through its concept of value types, which limit the way that state can be shared across API boundaries.


4 Answers

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
like image 65
Let's_Create Avatar answered Oct 13 '22 09:10

Let's_Create


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
    }
}
like image 21
DoesData Avatar answered Oct 13 '22 11:10

DoesData


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.

like image 3
trusk Avatar answered Oct 13 '22 09:10

trusk


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.

like image 3
kontiki Avatar answered Oct 13 '22 09:10

kontiki