Search code examples
swiftuinavigationview

Removing SwiftUI NavigationView from view hierarchy result in EXC_BAD_ACCESS


I am struggling with a bug and I just can't seem to solve it, or where to look further.

The problem occurs when I try to remove a view (which holds a NavigationView) from the view hierarchy. It crashes with: Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)

After experimenting with the sanitizer I got this output in the debugger: *** -[_TtGC7SwiftUI41StyleContextSplitViewNavigationControllerVS_19SidebarStyleContext_ removeChildViewController:]: message sent to deallocated instance 0x10904c880

Which pointed me to figure out that it was the NavigationView that cause it somehow. But I still can't figure out how to get from here.

This problem ONLY occurs on a real device, it works just fine in a simulator and you may have to hit the login, and then log out and log back in a few times before the crash happens.

I made a sample app with the example: https://github.com/Surferdude667/NavigationRemoveTest

The code is as follows:

NavigationRemoveTestApp

@main
struct NavigationRemoveTestApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
        }
    }
}

RootView

struct RootView: View {

    @StateObject private var viewModel = RootViewModel()

    var body: some View {
        if !viewModel.loggedIn {
            WelcomeView()
        } else {
            ContentView()
        }
    }
}

RootViewModel

class RootViewModel: ObservableObject {

    @Published var loggedIn = false

    init() {
        LogInController.shared.loggedIn
            .receive(on: DispatchQueue.main)
            .assign(to: &$loggedIn)
    }
}

WelcomeView

struct WelcomeView: View {
    var body: some View {
        NavigationView {
            VStack {
                Text("Welcome")
                NavigationLink("Go to login") {
                    LogInView()
                }
            }
        }
    }
}

LogInView

struct LogInView: View {
    var body: some View {
        VStack {
            Text("Log in view")
            Button("Log in") {
                LogInController.shared.logIn()
            }
        }
    }
}

ContentView

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Content view")
            Button("Log out") {
                LogInController.shared.logOut()
            }
        }
    }
}

LogInController

import Combine
class LogInController {

    static let shared = LogInController()

    var loggedIn: CurrentValueSubject<Bool, Never>

    private init() {
        self.loggedIn = CurrentValueSubject<Bool, Never>(false)
    }

    func logIn() {
        self.loggedIn.send(true)
    }

    func logOut() {
        self.loggedIn.send(false)
    }
}

Solution

  • I found a few solutions.

    1. Either you wrap the if statement in the RootView with a NavigationView instead of having the NavigationView inside the actual views, it works. This is however not very convenient since everything is now wrapped in a NavigationView.

    2. Replacing NavigationView with the new iOS 16 NavigationStack also solves it.