FB11835461
In real world applications it makes more sense to use ObservableObject referrence type to drive navigation than simple value type. The example below demonstrates that.
Problem is that for some reason NavigationStack will create memory leak if path is set to empty array after navigating any number of times through stack. All view models that were created in process will be retained in memory even though path is set to [].
Same problem occurs if we set path to empty array and just swap whole NavigationStack with any other view still all objects created will be retained in memory.
There is workaround that I will show bellow to fix this but it is still very buggy behaviour IMO.
struct ContentView: View {
@State var path: [ViewModel] = []
@State var showNavigationStack = true
@ObservedObject var rootVM: ViewModel
var body: some View {
if showNavigationStack {
NavigationStack(path: $path) {
ListView(vm: rootVM)
.navigationDestination(for: ViewModel.self) { detailVM in
ListView(vm: detailVM)
.toolbar {
ToolbarItem {
Button("Pop To Root") {
self.path.removeAll()
}
}
ToolbarItem {
Button("Navigate from Stack") {
self.path.removeAll()
self.showNavigationStack = false
}
}
}
}
}
} else {
Text("Now check Memory Graph Hierarchy")
}
}
}
struct ListView: View {
@ObservedObject var vm: ViewModel
var body: some View {
List {
ForEach(vm.rows, id: \.self) { row in
NavigationLink(value: ViewModel()) {
Text(row)
}
}
}
}
}
final class ViewModel: ObservableObject, Hashable {
@Published var rows: [String] = ["1", "2", "3", "4"]
static func == (lhs: ViewModel, rhs: ViewModel) -> Bool {
lhs === rhs
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self))
}
}
Workaround to fix this has two parts
- Set id to NavigationStack, after popping to root make small delay ~ 0.1s reset NavigationStack id
- If swapping whole NavigationStack with some other view add small delay (~ 0.1s) after setting path to empty array and before changing state that will swap the views
struct ContentView: View {
@State var path: [ViewModel] = []
@State var showNavigationStack = true
@State var id: String = UUID().uuidString
@ObservedObject var rootVM: ViewModel
var body: some View {
if showNavigationStack {
NavigationStack(path: $path) {
ListView(vm: rootVM)
.navigationDestination(for: ViewModel.self) { detailVM in
ListView(vm: detailVM)
.toolbar {
ToolbarItem {
Button("Pop To Root") {
self.path.removeAll()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.id = UUID().uuidString
}
}
}
ToolbarItem {
Button("Navigate from Stack") {
self.path.removeAll()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.showNavigationStack = false
}
}
}
}
}
}
.id(id)
} else {
Text("Now check Memory Graph Hierarchy")
}
}
}