swiftuimemory-leaks

SwiftUI NavigationStack + .navigationTitle causes memory leak when using @State view model


I'm seeing a memory leak in SwiftUI when using NavigationStack with .navigationTitle(...). Specifically, when a ViewModel is stored in @State inside a destination view, the view model's deinit is never called after popping the view, unless I remove .navigationTitle, .refreshable, or both, or even ScrollView.

Reproducible Example:

import SwiftUI

enum CoordinatorRoute: Hashable {
    case detail
}

@Observable
class ViewModel {
    init() {
        print("🟢 ViewModel initialized")
    }

    func action() {
        
    }

    deinit {
        print("❌ ViewModel deinitialized")
    }
}

struct ContentView: View {
    @State private var path: [CoordinatorRoute] = []

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Button("Go to Detail") {
                    path.append(.detail)
                }
            }
            .navigationTitle("Home") // Commenting this out prevents the leak
            .navigationDestination(for: CoordinatorRoute.self) { route in
                switch route {
                case .detail:
                    DetailView()
                }
            }
        }
    }
}

struct DetailView: View {
    @State var viewModel = ViewModel()

    var body: some View {
        ScrollView {
            VStack {
                Text("Detail View")
            }
            .navigationTitle("Detail") // Commenting this out prevents the leak
        }
        .refreshable {
            viewModel.action() // Commenting this out prevents the leak
        }
    }
}

Tested using Xcode 16.2 and iOS 18.3 (on both simulator and physical device).

Just to clarify: is the only real solution here to manually detect and avoid combinations like .navigationTitle + .refreshable when using navigation?


Solution

  • Okay, the most refined solution so far is to do the following:

    .refreshable { [weak viewModel] in
       viewModel?.action()
    }