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
}
}
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.
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.
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”.
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.
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)
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
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With