SwiftUI:如何避免在视图更新期间修改状态?

ala*_*dey 9 uilabel swift swiftui

我想在按下文本标签后更新它,但在我的应用程序运行时收到此错误(没有编译错误):[SwiftUI] 在视图更新期间修改状态,这将导致未定义的行为。

这是我的代码:

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
   }   
}
Run Code Online (Sandbox Code Playgroud)

Sco*_*ter 13

View如果您在不返回 a (因此无法使用或 手势)的函数中遇到此问题onAppear,另一种解决方法是将更新包装在异步更新中:

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

不过,我无法判断这是否是最佳实践。

编辑:我现在在苹果工作;这是一个可以接受的方法。另一种方法是使用符合ObservableObject.


pro*_*ero 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"
            }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这里,我正在修改@State视图主体内的变量body。问题在于@State变量的突变会导致视图更新,进而调用body视图上的方法。因此,在对 的呼叫进行到一半时body,又发起了对 的另一个呼叫body。这种情况可能会一直持续下去。

另一方面,@State变量可以onTapGestureblock中发生变化,因为它是异步的,并且直到更新完成后才会被调用。

例如,假设我想在用户每次点击文本视图时更改文本。我可以有一个@State变量isFlipped,当点击文本视图时,手势块中的代码会切换该isFlipped变量。由于它是一个特殊@State变量,因此该更改将驱动视图更新。由于我们不再处于视图更新过程中,因此我们不会收到“视图更新期间修改状态”警告。

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

Run Code Online (Sandbox Code Playgroud)

对于您的FlashcardView,您可能希望在视图本身之外定义卡片,并将其作为初始化参数传递到视图中。

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
Run Code Online (Sandbox Code Playgroud)

但是,如果您希望视图在自身更改card也更改(并不是说您必须其作为逻辑下一步执行),那么此代码是不够的。除了弄清楚突变如何以及在何处发生之外,您还需要导入Combine并重新配置card变量和类型本身。Card这是一个不同的问题。

长话短说:修改@State手势块内的变量。如果您想在视图本身之外修改它们,那么除了@State注释之外您还需要其他东西。@State仅供本地/私人使用。

(我使用的是 Xcode 11 beta 5)


Fab*_*ian 3

每次重绘时(如果状态变量发生变化)var body: some View都会重新评估。在您的情况下这样做会更改另一个状态变量,这将在没有缓解的情况下结束循环,因为每次重新评估时都会进行另一个状态变量更改。

SwiftUI 如何处理这个问题既不能保证稳定,也不能保证安全。这就是为什么 SwiftUI 警告您下次可能会因此崩溃。

无论是由于实现更改、突然触发边缘条件,还是在从同一变量读取文本时异步更改文本时运气不佳,都会给您带来垃圾字符串/崩溃。

在大多数情况下,你可能会没事,但这比平常更不那么有保证。