Post

Replies

Boosts

Views

Activity

Reply to SwiftUI crash: Simultaneous accesses to X, but modification requires exclusive access.
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() }
Oct ’23
Reply to SwiftUI crash: Simultaneous accesses to X, but modification requires exclusive access.
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
Oct ’23