Swift ForEach不必要地更新视图

Swift ForEach不必要地更新视图,swift,swiftui,Swift,Swiftui,这是我的代码: struct TestPageView:View{ @状态变量块:[字符串]=[“1”、“2”、“3”] var body:一些观点{ VStack{ ForEach(块,id:\.self){block in TestView(块,$blocks) } } } } 结构测试视图:视图{ @绑定变量块:[字符串] 让块:字符串 init(ublock:String,blocks:Binding){ 打印(“创建空视图”) self.block=块 自。_块=块 } var bod

这是我的代码:

struct TestPageView:View{
@状态变量块:[字符串]=[“1”、“2”、“3”]
var body:一些观点{
VStack{
ForEach(块,id:\.self){block in
TestView(块,$blocks)
}
}
}
}
结构测试视图:视图{
@绑定变量块:[字符串]
让块:字符串
init(ublock:String,blocks:Binding){
打印(“创建空视图”)
self.block=块
自。_块=块
}
var body:一些观点{
TextField(“测试视图”,文本:。常量(块),onCommit:{
打印(“已提交”)
blocks.append(“新元素”)
})
}
}
基本思想是我的ForEach在我的块上循环,显示它们。当用户在聚焦块时执行操作时(
onCommit
此处),我需要将新块添加到列表中。这段代码实际上是正确的;问题在于性能。对于上述打印语句,输出如下:

Created empty view
Created empty view
Created empty view
Committed
Created empty view
Created empty view
Created empty view
Created empty view
因此,ForEach似乎重新呈现了列表中的所有项。在这个简单的例子中,这不是一个问题,但在我真正的问题中,重新创建所有的
TestView
s是一个很大的任务,完全没有必要:我只需要添加新元素,其他一切都可以保持不变。有什么好办法吗


注意:
\.self
不是问题所在。我尝试过让“块”符合
可识别的
。ForEach仍然无法识别相同的ID,仍然会再次创建视图。

不幸的是,在没有足够的文档的情况下,SwiftUI会出现很多黑盒行为

在较高的级别上,SwiftUI尝试查看需要使哪个视图的主体无效并重新计算。主体重新渲染是一项相当昂贵的操作,因此SwiftUI仅在需要时才尝试执行此操作

也就是说,
init
应该非常便宜视图在SwiftUI构建和区分视图树时始终被初始化

您应该查看的是计算实体的次数,而不是调用
init
的次数

碰巧,
TestView
的主体每次都被重新计算。为什么?其原因几乎可以肯定是由于
@Binding var blocks

当任何
@状态
@绑定
@观察对象
发生更改时,SwiftUI将重新计算主体。因此,使用
@Binding var blocks
,告诉SwiftUI
TestView
取决于该状态,并且当
blocks
数组得到更新时,它会使ForEach中的所有
TestView
无效

相反,您可以通过父级
TestPageView
onCommit
闭包传递给
TestView
,并向
块添加新元素,父级
TestPageView
,它也是
块的所有者:

struct TestView: View {
   var block: String
   var onCommit: ((String) -> Void)? = nil
    
   var body: some View {
      TextField("Test view", text: .constant(self.block), onCommit: {
         self.onCommit?(self.block)
      })
   }
}
不幸的是,这(令我惊讶)也不能解决这种情况,因为出于某种原因,SwiftUI确定拥有closure属性意味着应该重新计算视图

幸运的是,有一种方法可以最终说服SwiftUI,我们知道视图实际上不需要按照
equalable
重新计算,这样我们就可以准确地告诉SwiftUI如何比较视图更改

extension TestView: Equatable {
   static func == (lhs: TestView, rhs: TestView) -> Bool {
      lhs.block == rhs.block
   }
}
并在使用视图时使用
.equalable
修改器:

TestView(block: block) { txt in
   self.blocks.append(txt)
}.equatable()


仅供参考,使用
.constant(value)
绑定除了预览之外没有什么意义。我把它放在这里,因为您在示例中使用了它。

不幸的是,您的第二个解决方案对我不起作用。视图仍将重新计算。这并不是说观看成本太高;当它们被创建时,它们执行一个特定的操作,我真的希望这个操作对于添加的每个新块只触发一次。实际上,我再次输入了print语句,我意识到可能print语句正在改变执行。我在初始化视图时使用了断点,添加新元素只会得到3个元素,而不是4个。这有点混乱;我想我承诺的那一个是没有被重新计算的,但是为什么其他两个呢?我想确认一下,当你说“重新计算”时,你是在谈论
init
还是
body
?这并不能解决
init
-我想在重新计算父体时总是调用
init
,我猜,因为它可能会根据init参数而改变,但也因为需要(重新)创建视图树。因此,视图对init来说应该是闪电般的快。我建议创建一个新问题,而不是更新旧问题
TestView(block: block) { txt in
   self.blocks.append(txt)
}.equatable()