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

3

10

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
   }   
}
Unilobed answered 10/8, 2019 at 14:10 Comment(0)
B
15

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.

Borkowski answered 11/7, 2021 at 17:6 Comment(3)
Thanks. This helped me with a problem I was having trouble fixingFireplace
I'm not getting how to implement this methodOlivine
what about new async/await maybe now instead DispatchQueue.main we should rather use MainActor.run { }Soracco
S
5
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)

Scottyscotus answered 11/8, 2019 at 21:13 Comment(2)
Thanks for your feedback, it makes sense. I realised I must've been infinite looping by updating the card text while the view was being drawn.Unilobed
Does anyone know if Apple will deny an app if this error is present. I am not referring to the infinite, just the Modifying state during view update, this will cause undefined behavior. My case everything works and its behaving without issue just the keyboard hangs for a second when the modal box is disappearing in the parent windowArchle
S
3

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.

Strawworm answered 10/8, 2019 at 16:46 Comment(1)
thanks, it looks like i have some sort of infinite loop happening.Unilobed

© 2022 - 2024 — McMap. All rights reserved.