当与新的导航堆栈结合时,发现@State有一个奇怪的行为——这是错误还是我做错了



我已将我的swiftui应用程序转换为使用NavigationStack(路径:$visibilityStack)编程管理的新的NavigationStack。在这样做的过程中,我发现@State的一个意想不到的行为,这让我认为这种观点没有被正确地否定。

事实上,当我用堆栈中的另一个视图替换视图时,@State变量保持其当前值,而不是在呈现新视图时进行初始化。

是虫子吗?这是误解吗(我的还是别人的:-)?欢迎您的想法。事实上,我看到的唯一解决方法是在另一个对象中保持状态并同步。。。

我创建了一个小型项目。要复制,请单击NavigationLink,然后单击"显示其他水果"按钮更改当前视图中的@State状态,然后单击水果按钮更改视图。新视图显示为以前的状态(showMoreText为true,尽管在初始化过程中声明为false)。在进行更多测试时,似乎也没有调用.onAppear。使用旧式NavigationView和isPresented时,视图已正确初始化。

这里有完整的代码(除了基本的应用程序),这应该是一个很好的教程。

编辑每个Yrb的答案:数据在模型水果的水果列表中处理,以保持我们的ViewController干净。

控制器FruitViewController负责调用新视图:

class Fruit: Hashable, Identifiable {
// to conform to Identifiable
var id: String

init(name: String) {
self.id = name
}

// to conform to Hashable which inherit from Equatable
static func == (lhs: Fruit, rhs: Fruit) -> Bool {
return (lhs.id == rhs.id)
}

// to conform to Hashable
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
let fruitList = [Fruit(name: "Banana"), Fruit(name: "Strawberry"), Fruit(name: "Pineapple")]
class FruitViewController: ObservableObject {
@Published var visibilityStack : [Fruit] = []
// the functions that programatically call a 'new' view
func openView(fruit: Fruit) {
visibilityStack.removeAll()
visibilityStack.append(fruit)
//        visibilityStack[0] = fruit    // has the same effect
}

// another one giving an example of what could be a deep link
func bananaAndStrawberry() {
visibilityStack.append(fruitList[0])
visibilityStack.append(fruitList[1])
}
}

提供NavigatoinStack:的主ContentView

struct ContentView: View {
@StateObject private var fruitViewController = FruitViewController()

var body: some View {
NavigationStack(path: $fruitViewController.visibilityStack) {
VStack {
Button("Pile Banana and Strawberry", action: bananaAndStrawberry)
.padding(40)
List(fruitList) {
fruit in NavigationLink(value: fruit) {
Text(fruit.id)
}
}
}
.navigationDestination(for: Fruit.self) {
fruit in FruitView(fruitViewController: fruitViewController, fruit: fruit)
}
}
}

func bananaAndStrawberry() {
fruitViewController.bananaAndStrawberry()
}
}

应初始化@State变量的子视图FruitView:

struct FruitView: View {
// the state that will change and not be initialised
@State private var showMoreText = false
@ObservedObject var fruitViewController: FruitViewController
var fruit: Fruit

var body: some View {
Text("Selected fruit: " + fruit.id)
if (showMoreText) {
Text("The text should disappear when moving to another fruit")
HStack(spacing: 10) {
ForEach(fruitList) {
aFruit in Button(aFruit.id) {
fruitViewController.openView(fruit: aFruit)
}
}
}
} else {
Button("Show other fruits", action: showButtons)
}
}

// let's change the state
func showButtons() {
showMoreText = true
}
}

Yrb答案后的附录:

我做了另一个练习,也许可以更好地解释我的观点。使用visibilityStack.append将3个视图添加到堆栈数组中。让我们调用初始状态:state 0。它将创建这样的堆栈:

_View 1 - state 0_
_View 2 - state 0_
_View 3 - State 0_ (the one shown on the screen)

现在让我们修改视图3的状态以获得:

_View 1 - state 0_
_View 2 - state 0_ 
_View 3 - State 1_ (the one shown on the screen)

你能告诉我当你使用visibilityStack.remove(at:1)删除视图2时发生了什么吗?

答案是:您将获得以下堆栈:

_View 1 - state 0_
_View 3 - State 0_ (the one shown on the screen)

因此视图2不会被破坏。堆栈中的最后一个视图是被销毁的视图。

就Yrb而言,NavigationStack似乎是一种混合方法,既有处理视图的能力,又有一种模型。

编辑添加:将强制导航堆栈查看与全新类型相同的视图而不是缓存它们的最小代码量是将.id(some Hashable)添加到视图声明中。

在这种情况下,Fruit是可散列的,所以我们可以添加.id(fruit)

NavigationStack(path: $fruitViewController.visibilityStack) {
...
.navigationDestination(for: Fruit.self) { fruit in FruitView(fruitViewController: fruitViewController, fruit: fruit).id(fruit)
}
} 

我想说Adkarma有道理。这种观点不是";去交织";正如人们所期望的那样,因为他们还没有简单地装上一种新水果。他们已经编辑了实际的导航路径。

问题是视图的身份被保留了,因为(简化/有根据的猜测警告)它是NavigationStack[0] as? FruitViewNavigationStack[0] as FruitView仍然存在。在RootView重新出现之前,NavigationStack不会完全清除它。NavigationStack在新视图出现之前不会清理任何视图。

因此,如果将FruitViewController更改为:

func openView(fruit: Fruit) {
visibilityStack.append(fruit)
}

NavigationStack将按预期工作,您将继续向堆栈中添加视图-我在FruitView中添加了countervisibilityStack,以使发生的事情更加清晰,并添加了一些消息,以使事情在处理和清理时更加清晰。

struct ContentView: View {
@StateObject private var fruitController = FruitViewController()

var body: some View {
NavigationStack(path: $fruitController.visibilityStack) {
VStack {
...
}
.onAppear() {
print("RootView Appeared")
}
.navigationDestination(for: Fruit.self) {
fruit in FruitView(fruit: fruit).environmentObject(fruitController)
}
}
}
struct FruitView: View {
// the state that will change and not be initialised
@State private var showMoreText = false
@State private var counter = 0
@EnvironmentObject var fruitViewController: FruitViewController
var fruit: Fruit

var body: some View {
VStack {
Text("Selected fruit: " + fruit.id)
Text("How many updates: (counter)")
if (showMoreText) {
Text("The text should disappear when moving to another fruit")
HStack(spacing: 10) {
Text("Who is in the fruitList")
ForEach(fruitViewController.fruitList) {
aFruit in Button(aFruit.id) {
counter += 1
fruitViewController.openView(fruit: aFruit)
}
}
}

HStack(spacing:10) {
Text("Who is in the visibility stack")
ForEach(fruitViewController.visibilityStack) {
aFruit in Button(aFruit.id) {
counter += 1
fruitViewController.openView(fruit: aFruit)
}
}

}
} else {
Button("Show other fruits", action: showButtons)
}
}.onDisappear() {
print("Fruit view (fruit.id) is gone")
}.onAppear() {
print("Hi I'm (fruit.id), I'm new here.")
//dump(env)
}
}

// let's change the state
func showButtons() {
showMoreText = true
}
}

但这似乎不是阿卡玛想要的行为。他们不想越陷越深。他们想要在相同的位置进行硬交换?对的NavigationStack似乎试图通过不破坏相同类型的现有视图来提高效率,这当然会使@State对象保持原样。

导航路径是由绑定到CCD_ 12内部的CCD_。NavigationStack继续相信它处于fruitViewController.visibilityStack[0]。。。因为它是。它似乎不关心包装器中超出其类型的内容。

保留的FruitView将重新运行主体代码,但由于它不是";新视图";(它仍然是很好的旧NavigationStack[0]作为FruitView)它不会硬刷新@State变量。

但是";我把它清零了"你说,但NavigationStack似乎有它的副本,但它不会,直到它可以显示一个新的视图,它不需要这样做,因为实际上NavigationStack[0]中还有一些东西。

我认为Adkarma很清楚这与深度链接有关,下面是我非常喜欢的一些文章:

  • https://www.pointfree.co/blog/posts/78-reverse-engineering-swiftui-s-navigationpath-codability
  • https://swiftwithmajid.com/2022/06/21/mastering-navigationstack-in-swiftui-deep-linking/

同样有趣的是:

  • https://swiftui-lab.com/swiftui-id/

就其价值而言;完成它";当Nav数组中有内容更改时,我只会用onReceive显式检查View中的变量,但我同意这似乎有点混乱。如果有人对导航路径的硬交换有一个更优雅的解决方案,最终在同一"视图"中的相同类型的视图,我也会感兴趣;距离";就像阿卡玛的例子一样。


struct FruitView: View {
// the state that will change and not be initialised
@State private var showMoreText = false
@State private var counter = 0
@EnvironmentObject var fruitViewController: FruitViewController
var fruit: Fruit
var body: some View {
VStack {
Text("Selected fruit: " + fruit.id)
Text("How many updates: (counter)")
if (showMoreText) {
Text("The text should disappear when moving to another fruit")
HStack(spacing: 10) {
Text("Who is in the fruitList")
ForEach(fruitViewController.fruitList) {
aFruit in Button(aFruit.id) {
counter += 1
fruitViewController.openView(fruit: aFruit)
}
}
}
} else {
Button("Show other fruits", action: showButtons)
}
}.onReceive(fruitViewController.$visibilityStack) { value in
counter = 0
showMoreText = false

}

}
// let's change the state
func showButtons() {
showMoreText = true
}
}

FWIWNavigationPath也将查看位置/类型对,而不是完整的内容,因此您仍然需要使用.onRecieve或在视图调用中使用.id(some Hashable)来查看它。

已编辑以添加:下面的函数";作品";(它会将用户发送到根视图一瞬间,然后导航回),直到时间减少到加载rootView所需的时间。

func jumpView(fruit: Fruit) {
visibilityStack.removeAll()
Task { @MainActor in
//The time delay may be device/app specific. 
try? await Task.sleep(nanoseconds: 500_000_000)
visibilityStack.append(fruit)
}
} 

相关内容

最新更新