Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI: How do I avoid modifying state during view update?

I want to update a text label after it is being pressed, but I am getting this error while my app runs (there is no compile error): [SwiftUI] Modifying state during view update, this will cause undefined behaviour.

This is my code:

import SwiftUI

var randomNum = Int.random(in: 0 ..< 230)

struct Flashcard : View {

    @State var cardText = String()

    var body: some View {    
      randomNum = Int.random(in: 0 ..< 230)
      cardText = myArray[randomNum].kana      
      let stack = VStack {        
        Text(cardText)
          .color(.red)
          .bold()
          .font(.title)
          .tapAction {
            self.flipCard()
          }
      }     
      return stack
    }

   func flipCard() {
      cardText = myArray[randomNum].romaji
   }   
}
like image 255
alamodey Avatar asked Aug 10 '19 14:08

alamodey


People also ask

What does @state mean in SwiftUI?

SwiftUI uses the @State property wrapper to allow us to modify values inside a struct, which would normally not be allowed because structs are value types. When we put @State before a property, we effectively move its storage out from our struct and into shared storage managed by SwiftUI.

Why does SwiftUI use some view?

First, using some View is important for performance: SwiftUI needs to be able to look at the views we are showing and understand how they change, so it can correctly update the user interface.

What is var body some view in SwiftUI?

var body: some View { In swiftUI we can see that View is a protocol as the document says, “You create custom views by declaring types that conform to the `View` protocol. Implement the required `body` property to provide the content and behavior for your custom view”.

What is a view SwiftUI?

SwiftUI provides the views and controls in the sameway as UIKit to present our content on the screen and as well to handle the user interactions. Views and controls are the visual building blocks of your app's user interface. Use them to present your app's content onscreen.


3 Answers

struct ContentView: View {
    @State var cardText: String = "Hello"
    var body: some View {
        self.cardText = "Goodbye" // <<< This mutation is no good.
        return Text(cardText)
            .onTapGesture {
                self.cardText = "World"
            }
    }
}

Here I'm modifying a @State variable within the body of the body view. The problem is that mutations to @State variables cause the view to update, which in turn call the body method on the view. So, already in the middle of a call to body, another call to body initiated. This could go on and on.

On the other hand, a @State variable can be mutated in the onTapGesture block, because it's asynchronous and won't get called until after the update is finished.

For example, let's say I want to change the text every time a user taps the text view. I could have a @State variable isFlipped and when the text view is tapped, the code in the gesture's block toggles the isFlipped variable. Since it's a special @State variable, that change will drive the view to update. Since we're no longer in the middle of a view update, we won't get the "Modifying state during view update" warning.

struct ContentView: View {
    @State var isFlipped = false
    var body: some View {
        return Text(isFlipped ? "World" : "Hello")
            .onTapGesture {
                self.isFlipped.toggle() // <<< This mutation is ok.
            }
    }
}

For your FlashcardView, you might want to define the card outside of the view itself and pass it into the view as a parameter to initialization.

struct Card {
    let kana: String
    let romaji: String
}

let myCard = Card(
    kana: "Hello",
    romaji: "World"
)

struct FlashcardView: View {
    let card: Card
    @State var isFlipped = false
    var body: some View {
        return Text(isFlipped ? card.romaji : card.kana)
            .onTapGesture {
                self.isFlipped.toggle()
            }
    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        return FlashcardView(card: myCard)
    }
}
#endif

However, if you want the view to change when card itself changes (not that you necessarily should do that as a logical next step), then this code is insufficient. You'll need to import Combine and reconfigure the card variable and the Card type itself, in addition to figuring out how and where the mutation going to happen. And that's a different question.

Long story short: modify @State variables within gesture blocks. If you want to modify them outside of the view itself, then you need something else besides a @State annotation. @State is for local/private use only.

(I'm using Xcode 11 beta 5)

like image 105
proxpero Avatar answered Nov 15 '22 11:11

proxpero


If you're running into this issue inside a function that isn't returning a View (and therefore can't use onAppear or gestures), another workaround is to wrap the update in an async update:

func updateUIView(_ uiView: ARView, context: Context) {
   if fooVariable { do a thing }
   DispatchQueue.main.async { fooVariable = nil }
}

I can't speak to whether this is best practices, however.

Edit: I work at Apple now; this is an acceptable method. An alternative is using a view model that conforms to ObservableObject.

like image 43
Scott Fister Avatar answered Nov 15 '22 10:11

Scott Fister


On every redraw (in case a state variable changes) var body: some View gets reevaluated. Doing so in your case changes another state variable, which would without mitigation end in a loop since on every reevaluation another state variable change gets made.

How SwiftUI handles this is neither guaranteed to be stable, nor safe. That is why SwiftUI warns you that next time it may crash due to this.

Be it due to an implementation change, suddenly triggering an edge condition, or bad luck when something async changes text while it is being read from the same variable, giving you a garbage string/crash.

In most cases you will probably be fine, but that is less so guaranteed than usual.

like image 24
Fabian Avatar answered Nov 15 '22 10:11

Fabian