toolbar` bottomBar disappears after rotating iPhone from portrait to landscape

I have a SwiftUI detail view with a native toolbar. On iPhone, the bottom toolbar appears correctly in portrait. After rotating the device to landscape, the bottom toolbar disappears. It does not come back unless the detail view is rebuilt.

I would like to keep the native toolbar appearance and behavior, especially the iOS toolbar/glass effect. I do not want to replace it with a custom safeAreaInset bar.

Environment

  • Platform: iOS
  • Current target/system: iOS 27
  • UI framework: SwiftUI
  • Device idiom: iPhone
  • The issue happens when rotating from portrait to landscape.

Expected behavior

The native bottom toolbar remains visible after device rotation.

Actual behavior

The native bottom toolbar is visible in portrait, but disappears after rotating to landscape.

Core code

The main view attaches toolbar content like this:

private var contentWithToolbarAndSheets: some View {
    coreLayout
        .slateNavigationBarTitleDisplayModeInline()
        .toolbar {
            #if os(iOS)
            ToolbarItem(placement: .principal) {
                VStack(spacing: 0) {
                    Text(String(localized: "第 \(scene.safeNumber) 场"))
                        .font(.headline)
                        .fontWeight(.semibold)
                        .lineLimit(1)

                    Text(scene.safeSetName)
                        .font(.subheadline)
                        .foregroundStyle(.secondary)
                        .lineLimit(1)
                }
            }
            #endif

            bottomBarContent
        }
        #if os(macOS)
        .navigationTitle(String(localized: "第 \(scene.safeNumber) 场"))
        .navigationSubtitle(scene.safeSetName)
        #endif
        .slateBottomBarBackgroundHidden()
}

The bottom toolbar content:

@ToolbarContentBuilder
private var bottomBarContent: some ToolbarContent {
    #if os(iOS)
    let bar = scriptBottomBar

    ToolbarItem(placement: .slateBottomBar) {
        bar.monitorButton
    }

    if #available(iOS 26.0, macOS 26.0, *) {
        ToolbarSpacer(.fixed, placement: .slateBottomBar)
    }

    ToolbarItem(placement: .slateBottomBar) {
        bar.soundRollButton
    }

    if #available(iOS 26.0, macOS 26.0, *) {
        ToolbarSpacer(.flexible, placement: .slateBottomBar)
    }

    ToolbarItem(placement: .status) {
        bar.principalContent
    }

    ToolbarItem(placement: .slateBottomBar) {
        bar.historyButton
    }

    if #available(iOS 26.0, macOS 26.0, *) {
        ToolbarSpacer(.fixed, placement: .slateBottomBar)
    }

    if isRecording {
        if #available(iOS 26.0, macOS 26.0, *) {
            ToolbarSpacer(.fixed, placement: .slateBottomBar)
        }

        ToolbarItem(placement: .slateBottomBar) {
            bar.trailingContent
        }
    }
    #endif
}

The compatibility wrappers are:

func slateBottomBarBackgroundHidden() -> some View {
    #if os(iOS)
    self.toolbarBackground(.hidden, for: .bottomBar)
    #else
    self
    #endif
}

extension ToolbarItemPlacement {
    static var slateBottomBar: ToolbarItemPlacement {
        #if os(iOS)
        .bottomBar
        #else
        .automatic
        #endif
    }
}

Workaround that made it reappear

Previously, I had a workaround that listened to size class and orientation changes, then forced the detail view to rebuild by clearing and restoring the selected scene:

#if os(iOS)
.onChange(of: horizontalSizeClass) { _, _ in
    forceRefreshByClearingSidebarSelection()
}
.onChange(of: verticalSizeClass) { _, _ in
    forceRefreshByClearingSidebarSelection()
}
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
    let orientation = UIDevice.current.orientation
    guard orientation.isPortrait || orientation.isLandscape else { return }
    forceRefreshByClearingSidebarSelection()
}
#endif

#if os(iOS)
private func forceRefreshByClearingSidebarSelection() {
    guard UIDevice.current.userInterfaceIdiom == .phone else { return }
    let currentSceneID = session.selectedSceneID
    session.selectedSceneID = nil
    DispatchQueue.main.async {
        if session.selectedSceneID == nil {
            session.selectedSceneID = currentSceneID
        }
    }
}
#endif

This made the toolbar reappear after rotation, but it is too heavy because it rebuilds the selected scene/detail view.

Things I tried

  1. Moved the center toolbar item from .status to .bottomBar.

    • Result: did not fix the disappearing toolbar.
  2. Kept native toolbar, added a local toolbarRefreshToken, updated it on horizontalSizeClass / verticalSizeClass changes, and attached .id(toolbarRefreshToken) to toolbar item contents.

    • Result: did not fix it.
  3. Removed .toolbarBackground(.hidden, for: .bottomBar).

    • Result: did not fix it.
  4. Replacing the toolbar with safeAreaInset(edge: .bottom) works visually in terms of persistence, but loses the native toolbar/glass behavior, so this is not acceptable for this app.

Question

Is this expected behavior for SwiftUI bottom toolbars in compact-height landscape on iPhone, or is this a SwiftUI toolbar invalidation bug?

Is there a recommended way to keep native .toolbar / .bottomBar behavior stable across portrait-to-landscape rotation without forcing the entire detail view to rebuild?



toolbar` bottomBar disappears after rotating iPhone from portrait to landscape
 
 
Q