Search code examples
iosswiftiphoneswiftuiios17

Thread 1: Simultaneous accesses to 0x104344480 SwiftUI IOS17


after updating to iOS 17 I encountered this error, there are no errors in the simulator and on a phone with iOS 16, on a phone with iOS 17 it constantly crashes when going through several views, here is an example of how I use the transition to going back through several views:

struct MainMenuView: View {
    @ObservedObject var navigationState: NavigationState
        
    var body: some View {
        NavigationStack {
            ZStack(alignment: .bottom) {               
                HStack {
                    Button {
                        if appSettings.resumeGame {
                            isShowingDisableModal = true
                        } else {
                            appSettings.resetGame()
                            navigationState.teamBuildingView = true
                        }
                    } label: {
                        Text(LocalizedStringKey("New Game"))
                    }
                    .buttonStyle(StartGameButtonStyle())
                    .disabled(isShowingDisableModal)
                    
                    if appSettings.resumeGame {
                        Button {
                            navigationState.continueScoreView = true
                        } label: {
                            Text(LocalizedStringKey("Continue"))
                        }
                        .buttonStyle(GameButtonStyle())
                        .disabled(isShowingDisableModal)
                    }
                }
            }
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    CustomNavigationButton(action: {
                        navigationState.settingsView = true
                    }, imageName: "gearshape")
                    .font(.title2)
                    .opacity(isShowingDisableModal ? 0.5 : 1.0)
                    .disabled(isShowingDisableModal)
                }
            }
            .navigationDestination(isPresented: $navigationState.settingsView) {
                SettingsView()
            }
            .navigationDestination(isPresented: $navigationState.teamBuildingView) {
                TeamBuildingView(navigationState: navigationState)
            }
            .navigationDestination(isPresented: $navigationState.continueScoreView) {
                TeamScoreView(navigationState: navigationState)
            }
        }
    }
}

After that I go through two views, and try from 4 views to return to the first

struct TeamScoreView: View {    
    @ObservedObject var navigationState: NavigationState
    
    var body: some View {
        ZStack {
          // SOME CODE
        }
        .navigationTitle(LocalizedStringKey("Team Scores"))
        .navigationBarBackButtonHidden(true)
        .toolbar {
            ToolbarItem(placement: .navigationBarLeading) {
                CustomNavigationButton(action: {
                    appSettings.resumeGame = true
                    navigationState.teamBuildingView = false
                    navigationState.continueScoreView = false
                }, imageName: "chevron.left")
                .opacity(appSettings.isGameOver ? 0 : 1)
            }
        }
        .navigationDestination(isPresented: $navigationState.startGameView) {
            StartGameView(navigationState: navigationState)
        }
    }

and here when I try to go back I get this error, there are also places further where I use a similar return after several views and everything crashes with the same error.

Може хтось з України може мені допомогти, або дати фітбек через діскорд чи якийсь відео зв'язок, напишіть мені, буду дуже вдячний


Solution

  • Copied from my answer in Apple forum,

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

    Usage:

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