SwiftUI crash: Simultaneous accesses to X, but modification requires exclusive access.

We're facing a reproducible crash on iOS 17 that seems to be related to our ToolbarItems. It happens when we tear down a NavigationStack (by setting all navigationDestination Bindings to false), where the same view is present multiple times in the stack. This happens in about 20% of the time and only on a real iOS device:

Simultaneous accesses to 0x144a23a60, but modification requires exclusive access. Previous access (a modification) started at SwiftUI`<redacted> + 12312 (0x1a5595a34).

The stacktrace goes from [UIBarButtonItem dealloc] -> AG::Subgraph::notify_observers() -> swift::runtime::AccessSet::insert(swift::runtime::Access*, void*, void*, swift::ExclusivityFlags)

The "Current access (a read) started at:" info points to a very similar stacktrace (see the screenshot I linked below).

I failed to attach the crash report to this post, but have uploaded it here: https://www.dropbox.com/scl/fi/7lrwkuhge0dmz1grv9r3r/SwiftUI-ToolbarItem-Crash.txt?rlkey=pu0x835y25uy38r605qflhhtm&dl=0.

This is the Stacktrace from Xcode: https://www.dropbox.com/scl/fi/fdijajvq60jd7wawlsdjr/SwiftUI-ToolbarItem-Crash.png?rlkey=m05ahqihliji32udbcmd6yz0a&dl=0

Is there anything I can do to narrow down the root cause? A workaround would be to remove the ToolbarItems and fake a navigation controller, but I would like to avoid such hacks.

I've prepared a demo to reproduce the crash. I don't know how to attach it directly, so here's a dropbox link: https://www.dropbox.com/scl/fi/s4x49bykhjrnh7yxybajn/swiftui_toolbar_crash_example.zip?rlkey=d6iquywpvb8y3j0sshj6kjvh3&dl=0. You should press: "Click Me", then again "Click me" and then "Reset".

I'm faced with the same problem on iOS 17, is there are any solutions to the problem other than a fake navigation bar?

@olonsky Thanks for the example. I think the root cause is that in our code bases the navigation is changed in two different runloops, giving SwiftUI conflicting information about the desired navigation state. The fix is to make sure that the navigation for all views is derived from a single value (nested enum or an array) that is changed in a single step.

btw: I don't understand why you need to defer changing the path using DispatchQueue.main.async. By removing this hack, the crash is gone.

.onReceive(coordinator.$path) { value in
//  DispatchQueue.main.async {
        path = value
//  }
}

I have a similar issue, any fix or workaround?

Ok, probably this is a bug in SwiftUI. We created an example that allows reproducing the crash on an iOS 17 device.

We filed feedback FB13238006 about this. Feel free to duplicate when you're affected by this.

import SwiftUI

/// SwiftUI Toolbar crasher
/// =======================
///
/// Steps to reproduce
/// ------------------
/// * launch this app on a physical device running iOS 17 (any stable or beta release).
/// * Tap the center button ("Go to view 2")
/// * Tap the center button ("Go to view 3")
/// * Tap the center button ("Go to view 4")
/// * Tap the center button ("Go to view 5")
/// * Tap the center button ("Pop to root 💣")
///
/// Expected result
/// ---------------
/// * The app navigates back to the root screen.
///
/// Actual result
/// -------------
/// * The app crashes in 30-50% of the attempts.
///
///
class ToolbarCrasher: ObservableObject {
    @Published var navTree: NavigationTree? = nil
}

enum NavigationTree {
    case one(One?)
    var one: One? {
        switch self {
        case let .one(one): one
        }
    }
    var isOne: Bool {
        switch self {
        case .one: true
        }
    }
    enum One {
        case two(Two?)
        var two: Two? {
            switch self {
            case let .two(two): two
            }
        }
        var isTwo: Bool {
            switch self {
            case .two: true
            }
        }
        enum Two {
            case three(Three?)
            var three: Three? {
                switch self {
                case let .three(three): three
                }
            }
            var isThree: Bool {
                switch self {
                case .three: true
                }
            }
            enum Three {
                case four
                var isFour: Bool {
                    switch self {
                    case .four: true
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    @StateObject var toolbarCrasher: ToolbarCrasher = .init()
    var body: some View {
        NavigationStack {
            VStack {
                Text("Root View")
                Button(
                    action: { toolbarCrasher.navTree = .one(nil) },
                    label: { Text("Go to view 2") }
                )
            }
            .navigationDestination(
                isPresented: .init(
                    get: { toolbarCrasher.navTree?.isOne ?? false },
                    set: { isForward in if isForward { toolbarCrasher.navTree = nil } }
                ),
                destination: {
                    ContentView2(toolbarCrasher: toolbarCrasher)
                }
            )
            .padding()
            .toolbar {
                ToolbarItem(placement: .topBarTrailing, content: {
                    Button(
                        action: { toolbarCrasher.navTree = nil },
                        label: { Text("Pop to root 💣") }
                    )
                })
            }
        }
    }
}

struct ContentView2: View {
    @ObservedObject var toolbarCrasher: ToolbarCrasher
    var body: some View {
        VStack {
            Text("View 2")
            Button(
                action: { toolbarCrasher.navTree = .one(.two(nil)) },
                label: { Text("Go to view 3") }
            )
        }
        .toolbar {
            ToolbarItem(placement: .topBarTrailing, content: {
                Button(
                    action: { toolbarCrasher.navTree = nil },
                    label: { Text("Pop to root 💣") }
                )
            })
        }
        .navigationDestination(
            isPresented: .init(
                get: { toolbarCrasher.navTree?.one?.isTwo ?? false },
                set: { isForward in if isForward { toolbarCrasher.navTree = .one(nil) } }
            ),
            destination: {
                ContentView3(toolbarCrasher: toolbarCrasher)
            }
        )
        .padding()
    }
}

struct ContentView3: View {
    @ObservedObject var toolbarCrasher: ToolbarCrasher
    var body: some View {
        VStack {
            Text("View 3")
            Button(
                action: { toolbarCrasher.navTree = .one(.two(.three(nil))) },
                label: { Text("Go to view 4") }
            )
        }
        .toolbar {
            ToolbarItem(placement: .topBarTrailing, content: {
                Button(
                    action: { toolbarCrasher.navTree = nil },
                    label: { Text("Pop to root 💣") }
                )
            })
        }
        .navigationDestination(
            isPresented: .init(
                get: { toolbarCrasher.navTree?.one?.two?.isThree ?? false },
                set: { isForward in if isForward { toolbarCrasher.navTree = .one(.two(nil)) } }
            ),
            destination: {
                ContentView4(state: toolbarCrasher)
            }
        )
        .padding()
    }
}

struct ContentView4: View {
    @ObservedObject var state: ToolbarCrasher
    var body: some View {
        VStack {
            Text("View 4")
            Button(
                action: { state.navTree = .one(.two(.three(.four))) },
                label: { Text("Go to view 5") }
            )
        }
        .toolbar {
            ToolbarItem(placement: .topBarTrailing, content: {
                Button(
                    action: { state.navTree = nil },
                    label: { Text("Pop to root 💣") }
                )
            })
        }
        .navigationDestination(
            isPresented: .init(
                get: { state.navTree?.one?.two?.three?.isFour ?? false },
                set: { isForward in if isForward { state.navTree = .one(.two(.three(nil))) } }
            ),
            destination: {
                ContentView5(toolbarCrasher: state)
            }
        )
        .padding()
    }
}

struct ContentView5: View {
    @ObservedObject var toolbarCrasher: ToolbarCrasher
    var body: some View {
        Text("View 5")
        Button(
            action: { toolbarCrasher.navTree = nil },
            label: { Text("Pop to root 💣") }
        )
    }
}

#Preview {
    ContentView()
}

I have the same issue.

Same issue here. iOS17.

Hello, we have provided a workaround, the drawback is basically there is a ~500ms delay on the toolbar appearance when you navigate back. Please use the default .toolbar for most parent views (The view that NavigationStack wraps) and then use .safeToolbar for all child screens.

// MARK: - safeToolbar
extension View {
    /// Adds a safe toolbar to the view with custom content.
    ///
    /// Use this modifier to safely apply a toolbar to the view. 
    /// This modifier is designed to handle potential crashes that may occur with the standard `toolbar` modifier on iOS 17.
    ///
    /// - Parameter content: A closure returning the custom content of the toolbar.
    /// - Returns: A modified view with the safe toolbar applied.
    /// - Note: This modifier automatically manages the presentation of the toolbar to prevent potential crashes on iOS 17.
    @ViewBuilder
    public func safeToolbar<Content: View>(@ViewBuilder _ content: @escaping () -> Content) -> some View {
        if #available(iOS 17.0, *) {
            modifier(SafeToolbarView(content))
        } else {
            toolbar(content: content)
        }
    }

    /// Adds a safe toolbar to the view with custom content.
    ///
    /// Use this modifier to safely apply a toolbar to the view. 
    /// This modifier is designed to handle potential crashes that may occur with the standard `toolbar` modifier on iOS 17.
    ///
    /// - Parameter content: A closure returning the custom content of the toolbar.
    /// - Returns: A modified view with the safe toolbar applied.
    /// - Note: This modifier automatically manages the presentation of the toolbar to prevent potential crashes on iOS 17.
    @ViewBuilder
    public func safeToolbar<Content: ToolbarContent>(@ToolbarContentBuilder _ content: @escaping () -> Content) -> some View {
        if #available(iOS 17.0, *) {
            modifier(SafeToolbarContent(content))
        } else {
            toolbar(content: content)
        }
    }
}

private struct SafeToolbarView<ToolbarContent: View>: ViewModifier {
    @State private var isPresented: Bool = true
    private let toolBarContent: () -> ToolbarContent
    init(@ViewBuilder _ content: @escaping () -> ToolbarContent) {
        self.toolBarContent = content
    }
    func body(content: Content) -> some View {
        content
            .onAppear { isPresented = true }
            .onDisappear { isPresented = false }
            .toolbar(content: { if isPresented { toolBarContent() } })
    }
}

private struct SafeToolbarContent<S: ToolbarContent>: ViewModifier {
    @State private var isPresented: Bool = true
    private let toolBarContent: () -> S
    init(@ToolbarContentBuilder _ content: @escaping () -> S) {
        self.toolBarContent = content
    }
    func body(content: Content) -> some View {
        content
            .onAppear { isPresented = true }
            .onDisappear { isPresented = false }
            .toolbar(content: { if isPresented { toolBarContent() } })
    }
}

Please see usage in my next comment

Usage of .safeToolbar

import SwiftUI

/// SwiftUI Toolbar crasher
/// =======================
///
/// Steps to reproduce
/// ------------------
/// * launch this app on a physical device running iOS 17 (any stable or beta release).
/// * Tap the center button ("Go to view 2")
/// * Tap the center button ("Go to view 3")
/// * Tap the center button ("Go to view 4")
/// * Tap the center button ("Go to view 5")
/// * Tap the center button ("Pop to root 💣")
///
/// Expected result
/// ---------------
/// * The app navigates back to the root screen.
///
/// Actual result
/// -------------
/// * The app crashes in 30-50% of the attempts.
///
///
class ToolbarCrasher: ObservableObject {
    @Published var navTree: NavigationTree? = nil
}

enum NavigationTree {
    case one(One?)
    var one: One? {
        switch self {
        case let .one(one): one
        }
    }
    var isOne: Bool {
        switch self {
        case .one: true
        }
    }
    enum One {
        case two(Two?)
        var two: Two? {
            switch self {
            case let .two(two): two
            }
        }
        var isTwo: Bool {
            switch self {
            case .two: true
            }
        }
        enum Two {
            case three(Three?)
            var three: Three? {
                switch self {
                case let .three(three): three
                }
            }
            var isThree: Bool {
                switch self {
                case .three: true
                }
            }
            enum Three {
                case four
                var isFour: Bool {
                    switch self {
                    case .four: true
                    }
                }
            }
        }
    }
}

struct ContentView: View {
    @StateObject var toolbarCrasher: ToolbarCrasher = .init()
    var body: some View {
        NavigationStack {
            VStack {
                Text("Root View")
                Button(
                    action: { toolbarCrasher.navTree = .one(nil) },
                    label: { Text("Go to view 2") }
                )
            }
            .navigationDestination(
                isPresented: .init(
                    get: { toolbarCrasher.navTree?.isOne ?? false },
                    set: { isForward in if isForward { toolbarCrasher.navTree = nil } }
                ),
                destination: {
                    ContentView2(toolbarCrasher: toolbarCrasher)
                }
            )
            .padding()
            .toolbar {
                ToolbarItem(placement: .topBarTrailing, content: {
                    Button(
                        action: { toolbarCrasher.navTree = nil },
                        label: { Text("Pop to root 💣") }
                    )
                })
            }
        }
    }
}

struct ContentView2: View {
    @ObservedObject var toolbarCrasher: ToolbarCrasher
    var body: some View {
        VStack {
            Text("View 2")
            Button(
                action: { toolbarCrasher.navTree = .one(.two(nil)) },
                label: { Text("Go to view 3") }
            )
        }
        .safeToolbar {
            ToolbarItem(placement: .topBarTrailing, content: {
                Button(
                    action: { toolbarCrasher.navTree = nil },
                    label: { Text("Pop to root 💣") }
                )
            })
        }
        .navigationDestination(
            isPresented: .init(
                get: { toolbarCrasher.navTree?.one?.isTwo ?? false },
                set: { isForward in if isForward { toolbarCrasher.navTree = .one(nil) } }
            ),
            destination: {
                ContentView3(toolbarCrasher: toolbarCrasher)
            }
        )
        .padding()
    }
}

struct ContentView3: View {
    @ObservedObject var toolbarCrasher: ToolbarCrasher
    var body: some View {
        VStack {
            Text("View 3")
            Button(
                action: { toolbarCrasher.navTree = .one(.two(.three(nil))) },
                label: { Text("Go to view 4") }
            )
        }
        .safeToolbar {
            ToolbarItem(placement: .topBarTrailing, content: {
                Button(
                    action: { toolbarCrasher.navTree = nil },
                    label: { Text("Pop to root 💣") }
                )
            })
        }
        .navigationDestination(
            isPresented: .init(
                get: { toolbarCrasher.navTree?.one?.two?.isThree ?? false },
                set: { isForward in if isForward { toolbarCrasher.navTree = .one(.two(nil)) } }
            ),
            destination: {
                ContentView4(state: toolbarCrasher)
            }
        )
        .padding()
    }
}

struct ContentView4: View {
    @ObservedObject var state: ToolbarCrasher
    var body: some View {
        VStack {
            Text("View 4")
            Button(
                action: { state.navTree = .one(.two(.three(.four))) },
                label: { Text("Go to view 5") }
            )
        }
        .safeToolbar {
            ToolbarItem(placement: .topBarTrailing, content: {
                Button(
                    action: { state.navTree = nil },
                    label: { Text("Pop to root 💣") }
                )
            })
        }
        .navigationDestination(
            isPresented: .init(
                get: { state.navTree?.one?.two?.three?.isFour ?? false },
                set: { isForward in if isForward { state.navTree = .one(.two(.three(nil))) } }
            ),
            destination: {
                ContentView5(toolbarCrasher: state)
            }
        )
        .padding()
    }
}

struct ContentView5: View {
    @ObservedObject var toolbarCrasher: ToolbarCrasher
    var body: some View {
        Text("View 5")
        Button(
            action: { toolbarCrasher.navTree = nil },
            label: { Text("Pop to root 💣") }
        )
    }
}

#Preview {
    ContentView()
}

Looks like it's fixed in iOS 17.2 Beta

Fixed: Resolved a possible Swift access conflict crash that could occur with toolbar items. (113992797)

SwiftUI crash: Simultaneous accesses to X, but modification requires exclusive access.
 
 
Q