Skip to content

Instantly share code, notes, and snippets.

@iOSappssolutions
Last active May 12, 2023 13:44
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iOSappssolutions/2cd52a7b0df55bf7c53062e8ee890121 to your computer and use it in GitHub Desktop.
Save iOSappssolutions/2cd52a7b0df55bf7c53062e8ee890121 to your computer and use it in GitHub Desktop.
NavigationStack creates memory leak if Hashable referrence type is used for path

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

  1. Set id to NavigationStack, after popping to root make small delay ~ 0.1s reset NavigationStack id
  2. 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")
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment