SwiftUI 4:当state改变时,navigationDestination()的目标视图没有更新。

5

在尝试 Swift UI 4 中新的 NavigationStack 的过程中,我发现当状态改变时,navigationDestination() 返回的目标视图没有得到更新。请参阅下面的代码。

struct ContentView: View {
    @State var data: [Int: String] = [
        1: "One",
        2: "Two",
        3: "Three",
        4: "Four"
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach(Array(data.keys).sorted(), id: \.self) { key in
                    NavigationLink("\(key)", value: key)
                }
            }
            .navigationDestination(for: Int.self) { key in
                if let value = data[key] {
                    VStack {
                        Text("This is \(value)").padding()
                        Button("Modify It") {
                            data[key] = "X"
                        }
                    }
                }
            }
        }
    }
}

问题重现步骤:

  1. 运行代码并点击列表中的第一项。这将带您进入该项的详细视图。

  2. 详细视图显示了该项的值。它还有一个修改该值的按钮。单击该按钮。您会发现详细视图中的值不会更改。

我通过在不同位置设置断点来调试问题。我的观察结果:

  • 当我点击按钮时,List中的代码被执行。这是预期的。

  • 但是传递给 navigationDestination() 的闭包未被执行,这解释了为什么详细视图没有更新。

是否有人知道这是一个bug还是预期行为? 如果这不是一个bug,那么我该如何编程以更新详细视图中的值?

顺便说一下,如果我回到根视图并再次点击第一项以进入其详细视图,则传递给navigationDestination()的闭包将被执行,并且详细视图将正确显示修改后的值。


2
这是预期的行为,概念与表格(item、alerts等)相同。在闭包内部(视图层次结构之外),您拥有不同的上下文,因此外部状态不会刷新它。请改用独立视图和绑定。 - Asperi
@Asperi,您的解释非常有价值。通过“闭包内部具有不同的上下文”,您是否意味着闭包隐式地捕获了“data”,因此无法看到最新的“data”值?这对我来说很有道理。我很惊讶自己在SwiftUI编程中没有意识到这个微妙的问题是如何存活下来的。我认为您的建议与@NoeOnJupiter给出的代码相同。但是,我不确定他的解释是否正确。他说:“navigationDestination仅在激活NavigationLink时调用,而不是在State更改时调用。”我不认为那是正确的。 - rayx
我的假设有些问题。在这个假设中,我假设闭包被调用并返回了一个包含过期数据的视图,因为它捕获的data值已经过期。然而,正如我在原始问题中所描述的那样,我在闭包中设置了断点,但当我按下“修改”按钮时,它没有被触发。所以这个闭包没有被调用。嗯...对我来说,一个视图修饰符如何确定是否调用它的闭包参数似乎是个谜。 - rayx
刚刚意识到我又犯了一个错误。我说闭包捕获了data,那是错的。我相信实际上被捕获的是一个不可变的self副本(整个结构体的值)。这个self稍后会被“Modify It”按钮动作闭包修改。但是,如果State包装器将实际值存储在内存中相同的位置,我认为旧的self和新的self应该共享相同的data值,因此根本没有这样的问题。所以似乎data在内存中的实际位置一直在改变?这是我不知道的SwiftUI实现细节。 - rayx
使用id:.self的ForEach通常是错误的,不确定人们从哪里学到这个。 - malhal
显示剩余3条评论
2个回答

3
@NoeOnJupiter的解决方案和@Asperi的评论非常有帮助。但正如您在上面的评论中看到的,我对一些细节不确定。以下是我最终理解的摘要,希望能澄清混淆。
1. `navigationDestination()`接受一个闭包参数。该闭包捕获了`self`的不可变副本。 顺便说一下,SwiftUI利用属性包装器使得“修改”不可变值成为可能,但我们不会在这里讨论细节。
2. 以我的上面的代码为例,由于使用了`@State`包装器,不同版本的`ContentView`(即在闭包中捕获的`self`)共享相同的数据值。 关键点在于我认为闭包实际上可以访问最新的`data`值
3. 当用户点击“修改”按钮时,`data`状态发生变化,导致重新评估`body`。由于`navigationDestination()`是`body`中的函数,因此也被调用。但是,修改器函数只是`modifier(SomeModifier())`的快捷方式。实际的工作在于其`body`中。仅因为调用了修改器函数并不意味着相应的`Modifier`的`body`被调用。后者是一个谜题(苹果未披露且难以猜测的实现细节)。例如,参见此帖(作者是苹果开发者论坛中的高声望用户):
“在我看来,这肯定是一个错误,但不确定苹果是否会很快修复它。 一个解决方法是传递一个Binding而不是@State变量的值。”
顺便说一下,我对此有一个假设。也许这基于类似SwiftUI确定是否调用子视图的`body`的方法? 我猜测这可能是一种设计,而不是错误。由于某种原因(性能?),SwiftUI团队决定缓存`navigationDestination()`返回的视图,直到重建`NavigationStack`。作为用户,我发现这个行为很困惑,但这并不是SwiftUI中不一致行为的唯一例子。
因此,与我先前的想法不同,这不是闭包的问题,而是修改器工作方式的问题。幸运的是,正如@NoeOnJupiter和@Asperi建议的那样,有一个众所周知且强大的解决方法。 更新:另一种解决方法是使用EnvironmentObject,当数据模型更改时,它会导致占位符视图的正文被重新调用。我最终采用了这种方法,它非常可靠。在我的简单实验中,绑定方法有效,但在我的应用程序中无效(当数据模型更改时,占位符视图的正文没有被重新调用)。我花了一天多的时间来解决这个问题,但不幸的是,在绑定突然停止工作时,我找不到任何调试方法。

你能发布一下你最终的“修复”示例吗?我无法理解你所说的“另一种解决方案是使用EnvironmentObject来导致占位符视图的body被重新调用”。谢谢。 - Mihai Fratu
1
它与Timmy的代码类似。唯一的区别是他在SubContentView中使用Binding,而我使用EnvironmentObject - rayx

1
按钮正确地更改了值。默认情况下,导航目标不会在父子之间创建绑定关系,使传递的值是不可变的。因此,您应该为子元素创建一个单独的结构体,以实现可绑定行为。
struct ContentView: View {
    @State var data: [Int: String] = [
        1: "One",
        2: "Two",
        3: "Three",
        4: "Four"
    ]

    var body: some View {
        NavigationStack {
            List {
                ForEach(Array(data.keys).sorted(), id: \.self) { key in
                    NavigationLink("\(key)", value: key)
                }
            }
            .navigationDestination(for: Int.self) { key in
                SubContentView(key: key, data: $data)
            }
        }
    }
}

struct SubContentView: View {
    let key: Int
    @Binding var data: [Int: String]
    var body: some View {
        if let value = data[key] {
            VStack {
                Text("This is \(value)").padding()
                Button("Modify It") {
                    data[key] = "X"
                }
            }
        }
    }
}

这是一个很好的解决方案。由于某种原因,当我们通过导航组件传递集合中的元素值时,除了像整数/字符串这样的单个元素或像数组/字典这样的整体之外,它根本不起作用。 - Steven-Carrot
1
谢谢你的解决方案。它有效。但是,我不确定你关于navigationDestination()在状态更改时不会被调用的解释是否正确。请查看我对@Asperi的问题的更多描述。欢迎添加您的评论。谢谢。 - rayx
1
@tail 这是 Swift 中闭包的一个特性。请参见我上面的评论。我对此非常熟悉,但我不知道在 SwiftUI 中有这个具体的用例。上面的解决方案之所以有效,是因为它捕获了绑定,而不是不可变(和过时的)值。 - rayx
我所说的“By doesn't get called when State changes”是指View没有与其父视图创建任何类型的Binding。请原谅我的术语不足。 - Timmy
1
你说的关于 View 的内容是正确的。但我认为这有点不相关。我们在这里讨论的是一个视图修饰符,即 navigationDestination(),它是 body 中的一个函数。由于 body 会被重新评估,因此 navigationDestination() 也会被调用。这就是我的理解。 - rayx
1
嗨@NoeOnJupiter,后来我意识到你所说的(当状态改变时navigationDestination()不会被调用)是正确的。这与我的调试结果相符。只是为什么会这样还不清楚。我搜索了一下,发现没有人确切知道。这是苹果不公开的实现细节。我创建了一个答案来总结我的理解。对于造成的困惑,非常抱歉并感谢! - rayx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接