为什么我的 SwiftUI 自定义视图没有正确更新?

时间:2021-07-18 15:54:02

标签: view swiftui state

struct ContentView: View {
    @State var items = ["red", "blue", "green"]
    var body: some View {
        VStack {
            ForEach(items.indices, id: \.self) { idx in
                ItemView(str: items[idx])
            }
            Button("Replace items") {
                items = [ "pig", "dog", "horse" ]
            }
        }.frame(width: 200, height: 150)
    }
}

struct ItemView: View {
    @State var itemstr = ""
    let str: String
    var body: some View {
        HStack {
            Text(str)
            TextField("New: "+str, text: $itemstr)
        }
    }
}

这是我看到的情况: Initial screen

输入一些文字。

after I type in some text

现在按下“替换项目”按钮。

press button to change underlying state

现在可以看到 TextField @State 并且“str”标签数据不一致。 “pig”来自刷新的视图,“rreedd”来自视图的旧副本。

文本上的退格确认初始值也不一致。

backspace over the text to see the TextField initial value.

看起来 ItemView 自定义视图正在使用新的初始值设定项参数 (str) 但使用旧状态 (itemstr) 进行重建。

我真正的问题比这更复杂。但它涉及使用 ForEach 创建的子视图,并且在模型数据更改时 ForEach 不会按照我期望的方式重新迭代。

我不确定这是否是 SwiftUI 的错误,因为我没有深入了解 SwiftUI 用于传播视图更改的算法。

好像是创建了一个新视图,将新视图的内部状态与旧视图的内部状态进行比较,然后决定是重新计算子视图并使用新视图还是坚持使用旧视图。但我从未在官方文档中看到过这一点。

1 个答案:

答案 0 :(得分:2)

这种行为是设计使然。

在 SwiftUI 中,视图会定期初始化(每当它们的父级重新计算其 body 时),并且状态决定了视图是否实际发生了变化(这意味着它也需要反过来,重新计算它自己的 body)。

因此,当您声明 @State var itemstr = "" 时,您给该状态属性一个 initial"",但随后视图在初始化之间保持其状态。 >


那你如何重置状态?

在您的示例中,我认为,每当 ItemView 更改时,str 都会想要重置其状态。您需要使用 onChange:

明确执行此操作
struct ItemView: View {
    @State var itemstr = ""
    let str: String
    
    var body: some View {
        HStack {
            Text(str)
            TextField("New: "+str, text: $itemstr)
        }
        .onChange(of: str, perform: { _ in
            itemstr = "" // reset whenever str changes
        })
    }
}

或者,虽然不一定适合这个例子,但父级可以通知 SwiftUI 它的主体,尽管视图结构看起来没有变化,但实际上已经完全改变,状态应该被重置。

在这种情况下,您可以使用 .id 视图修饰符并提供 items[idx] 作为每个项目的 id - 这意味着当 items[idx] 更改时意味着视图它下面的层次结构也发生了变化。

采用这种方法,您只需更新父级,而可以保持 ItemView 原样:

ForEach(items.indices, id: \.self) { idx in
    ItemView(str: items[idx])
       .id(items[idx])
}