Mobile Development

Fix EXC_BAD_ACCESS @StateObject in SwiftUI NavigationViews

Learn how to resolve EXC_BAD_ACCESS crashes in SwiftUI when using @StateObject inside nested NavigationViews with step-by-step debugging tips.

IMTechy
IMTechy
2 Sept 2026
8 min read
0 views
Fix EXC_BAD_ACCESS @StateObject in SwiftUI NavigationViews

Introduction

Ever had your SwiftUI app crash mid‑navigation with that dreaded EXC_BAD_ACCESS? I remember the first time it hit me mid‑night, a new feature, and a stack trace that read:

Thread 1: EXC_BAD_ACCESS (code=1, address=0x0000000000)

It wasn’t a typo, it wasn’t a missing import. It was the state object getting released while a view was still trying to read it. In this post I’ll walk through why that happens when you nest NavigationViews, how to reproduce it, and, most importantly, how to stop it.

What is EXC_BAD_ACCESS?

EXC_BAD_ACCESS is a low‑level crash that tells the runtime you tried to access a memory address that’s no longer valid. In Swift, it often surfaces when you hold a reference to an object that has already been deallocated. The typical error message you’ll see in Xcode:

Thread 1: EXC_BAD_ACCESS (code=1, address=0x0000000000)

It’s the equivalent of dereferencing a null pointer in C. The crash is silent in the sense that the app just dies, but the stack trace usually points to a line in your SwiftUI view where a property is accessed.

@StateObject Basics

@StateObject is SwiftUI’s way of owning an observable object. You create it once, and SwiftUI keeps it alive for the lifetime of the view hierarchy that owns it.

Wrong

struct ProfileView: View {
    @StateObject var viewModel = ProfileViewModel() // ❌
    
    var body: some View {
        Text(viewModel.name)
    }
}

If ProfileView is recreated multiple times (e.g., inside a NavigationLink), SwiftUI will re‑initialize viewModel each time, potentially releasing the old instance. That’s a recipe for EXC_BAD_ACCESS if another view still references the old object.

Right

class ProfileViewModel: ObservableObject {
    @Published var name = "John Doe"
}

struct ProfileView: View {
    @StateObject private var viewModel = ProfileViewModel()
    
    var body: some View {
        Text(viewModel.name)
    }
}

Here, @StateObject is marked private and the view owns a single instance. SwiftUI guarantees it stays alive as long as the view is in the hierarchy.

Tip: Never initialize a @StateObject inside a computed property or a function that can be called multiple times.

Nested NavigationViews and Their Pitfalls

When you place a NavigationView inside another NavigationView, you create two independent navigation stacks. If each stack owns its own @StateObject, they can get out of sync, and a view might try to read a state object that has already been deallocated.

Wrong

struct RootView: View {
    var body: some View {
        NavigationView {
            NavigationLink("Go to Details", destination: DetailView())
        }
    }
}

struct DetailView: View {
    @StateObject var detailVM = DetailViewModel()
    
    var body: some View {
        NavigationView { // ❌ nested
            Text(detailVM.info)
        }
    }
}

The inner NavigationView creates a new navigation stack. If the user goes back to RootView and the outer stack is popped, detailVM may be released while the inner navigation stack still holds a reference to it.

Right

struct RootView: View {
    var body: some View {
        NavigationView {
            NavigationLink("Go to Details", destination: DetailView())
        }
    }
}

struct DetailView: View {
    @StateObject private var detailVM = DetailViewModel()
    
    var body: some View {
        VStack {
            Text(detailVM.info)
            NavigationLink("Next", destination: NextView())
        }
    }
}

By removing the nested NavigationView, the entire navigation stack is shared, and the @StateObject stays alive as long as the user is on DetailView or any of its children.

Reproducing the Crash

Let’s build a minimal example that crashes on a back navigation.

class CounterVM: ObservableObject {
    @Published var count = 0
}

struct CounterView: View {
    @StateObject var vm = CounterVM()
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Count: \(vm.count)")
                Button("Increment") { vm.count += 1 }
                NavigationLink("Next", destination: NextView())
            }
        }
    }
}

struct NextView: View {
    @StateObject var vm = CounterVM() // new instance
    var body: some View { Text("Next") }
}

Run this, hit “Increment” a few times, tap “Next”, then press back. The app crashes with:

Thread 1: EXC_BAD_ACCESS (code=1, address=0x0000000000)

Why? The CounterVM in CounterView was released when the outer NavigationView was popped, but NextView still holds a reference to its own CounterVM that was never released, causing a dangling pointer when the view hierarchy is torn down.

Root Causes of the Crash

  1. Multiple ownership – Two views each own a @StateObject that should be shared.

  2. Nested navigation stacks – Each stack has its own lifecycle, so objects can be released unexpectedly.

  3. Improper initialization – Initializing @StateObject in a computed property or inside a function that gets called multiple times.

  4. Weak references in closures – Capturing a view model weakly inside a closure that outlives the view.

When SwiftUI deallocates a view, it also deallocates its @StateObject. If another view still references that instance, the runtime throws EXC_BAD_ACCESS.

Common Fixes

1. Promote the state object to a higher level

class GlobalCounter: ObservableObject {
    static let shared = GlobalCounter()
    @Published var count = 0
}

struct CounterView: View {
    @StateObject private var vm = GlobalCounter.shared
    
    var body: some View { /* ... */ }
}

Now all views share the same instance, so it won’t be deallocated unexpectedly.

2. Use @EnvironmentObject for shared state

struct CounterView: View {
    @EnvironmentObject var vm: GlobalCounter
    // ...
}

Inject the object once at the root:

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            CounterView()
                .environmentObject(GlobalCounter.shared)
        }
    }
}

3. Remove nested NavigationViews

As shown earlier, keep a single navigation stack.

4. Avoid re‑initializing in NavigationLink

NavigationLink(destination: DetailView()) { Text("Go") }

Don’t create a new @StateObject inside the destination if you need to preserve state across navigation.

Using @EnvironmentObject for Shared State

@EnvironmentObject is designed for data that needs to flow through many views. It solves the ownership problem by letting a single source of truth live at the top of the view hierarchy.

class AuthManager: ObservableObject {
    @Published var isLoggedIn = false
}

struct ContentView: View {
    @StateObject private var auth = AuthManager()
    
    var body: some View {
        NavigationView {
            if auth.isLoggedIn {
                DashboardView()
            } else {
                LoginView()
            }
        }
        .environmentObject(auth) // inject
    }
}

Both DashboardView and LoginView can now access auth via @EnvironmentObject without worrying about lifecycle.

Proper Lifecycle Management

When you need a view model that lives only as long as a specific view, use @StateObject. When the object should outlive the view (e.g., a network manager), use @ObservedObject or @EnvironmentObject.

struct DetailView: View {
    @ObservedObject var vm: DetailVM // injected from parent
    
    var body: some View { /* ... */ }
}

This pattern ensures the parent owns the object, preventing accidental deallocation.

Debugging Tools & Techniques

  1. Xcode Memory Graph – Capture a snapshot when the crash occurs. Look for dangling references or unexpected deallocations.

  2. Enable Zombies – In Xcode’s scheme diagnostics, turn on “Enable Zombie Objects”. This turns deallocated objects into “zombies” that throw a descriptive exception instead of an EXC_BAD_ACCESS.

  3. Print Deinit – Add a deinit to your view model and log when it’s called.

class CounterVM: ObservableObject {
    @Published var count = 0
    deinit { print("CounterVM deinit") }
}

If you see the deinit log before the crash, you know the object is being released too early.

  1. Use the swift debugger – Set a breakpoint on deinit and watch the stack when it’s hit.

Quick Wins to Avoid Future Crashes

  • One navigation stack per screen flow – Don’t nest NavigationViews unless you truly need a separate stack.

  • Inject shared objects at the top – Use environmentObject for global state.

  • Prefer @StateObject for view‑local state – Keep it private and avoid re‑initialization.

  • Turn on Zombies during development – It’s a quick way to catch dangling references before shipping.

Real‑world scenario: In a payment app, the checkout flow uses nested NavigationViews for the cart and payment screens. When a user goes back from the payment screen, the cart’s @StateObject is deallocated while the payment screen still holds a reference, leading to a crash. By moving the cart’s view model to a shared @EnvironmentObject injected at the app launch, the crash disappears.

Wrapping Up

I’ve spent countless nights debugging EXC_BAD_ACCESS crashes that looked like random bugs. The real culprit was usually a mismatch between view ownership and the SwiftUI view hierarchy. By keeping a single navigation stack, promoting shared state to @EnvironmentObject, and being mindful of when @StateObject is initialized, I’ve turned those night‑time panic sessions into smooth releases.

If you’re building a complex navigation flow, think about who truly owns each piece of state. It’s not just about “does it compile?” it’s about “does it survive the user’s journey?”

For more on building robust payment flows, check out How to Build a Fully Functional Payment Gateway. It dives into state management across multiple screens.

FAQs

  1. Why does EXC_BAD_ACCESS happen even though I’m using @StateObject?
    It usually means the object was deallocated while a view still referenced it often due to nested navigation stacks or re‑initializing the object in a computed property.

  2. Can I use @ObservedObject instead of @StateObject to avoid crashes?
    @ObservedObject assumes the owner lives elsewhere. If you create it inside the view, you risk the same deallocation issue. Use @StateObject for view‑owned objects.

  3. How do I debug a crash that says EXC_BAD_ACCESS (code=1, address=0x0000000000)?
    Enable Zombies, check the memory graph, and add deinit prints to track when objects are released.

  4. Is it okay to nest NavigationViews for modal presentations?
    Only if you truly need a separate navigation stack. Otherwise, use sheet or fullScreenCover to present modals.

  5. What’s the difference between @EnvironmentObject and passing objects via initializer?
    @EnvironmentObject is great for global, shared state; initializer injection is better for tightly coupled view models that don’t need to be shared.

Happy coding, and may your SwiftUI apps stay crash‑free!

Tags:SwiftUIEXC_BAD_ACCESS@StateObjectNavigationViewiOS debugging
Share this article:
Sameer Singh

Written by

Sameer Singh

Founder & Technology Writer

Expertise in AI, Web Development & Cybersecurity. Passionate about making complex technology accessible and actionable for everyone.