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()
}