I have transitioned my swiftui app to the new NavigationStack programmatically managed using NavigationStack(path: $visibilityStack). While doing so, I found an unexpected behaviour of @State that makes me think the view is not dismissed correctly.
In fact, when I am replacing the view with another one in the stack, the @State variable is keeping its current value instead of being initialised, as it should be when presenting a new view.
Is it a bug? Is it a misconception (mine or someone else :-))? Your thoughts are welcome. As it is, the only workaround I see is to maintain a state in another object and synchronise...
I have created a mini-project. To reproduce, click on the NavigationLink, then click on the 'show other fruits' button to change the @State in the current View, then click a fruit button to change the view. The new view appears with the previous state (showMoreText is true, although it is declared as false during init). While doing more tests, it also appears that .onAppear is not called either. When using the old style NavigationView and isPresented, views were correctly initialised.
Full code here (except App which is the basic one), which should have been a good tutorial.
EDIT per Yrb answer: The data is handled in the fruitList of Model Fruit to keep our ViewController clean.
The controller FruitViewController is responsible to call the new views:
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])
}
}
The main ContentView which provides the NavigatoinStack:
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()
}
}
The subview FruitView where the @State variable should be initialised:
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
}
}
ADDENDUM after Yrb answer:
I have done another exercise, to maybe better explain my point. Add 3 views to the stack array with visibilityStack.append . Let's call the initial state: state 0. It will create a stack like this:
_View 1 - state 0_
_View 2 - state 0_
_View 3 - State 0_ (the one shown on the screen)
Let's now modify the state of our View 3 to obtain:
_View 1 - state 0_
_View 2 - state 0_
_View 3 - State 1_ (the one shown on the screen)
Can you tell me what is happening when you remove View 2 using visibilityStack.remove(at: 1)?
The answer is: you will obtain the following stack:
_View 1 - state 0_
_View 3 - State 0_ (the one shown on the screen)
So the View 2 is not destroyed. The last View in the stack is the one that is destroyed.
To Yrb point, it seems like if NavigationStack was a mixed approach between the ability to deal with the views, but with a kind of Model in mind.