首页 > 解决方案 > 为什么我的 SwiftUI 自定义视图没有正确更新?

问题描述

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)
        }
    }
}

这是我看到的情况: 初始画面

输入一些文字。

在我输入一些文字后

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

按下按钮改变底层状态

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

Backspace 确认的文本初始值也不一致。

在文本上退格以查看 TextField 初始值。

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

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

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

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

标签: viewswiftuistate

解决方案


此行为是设计使然。

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

因此,当您声明 时@State var itemstr = "",您为该状态属性赋予初始"",但是视图在初始化之间保持其状态。


那你如何重置状态?

在您的示例中,我认为,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])
}

推荐阅读