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
-
Moved the center toolbar item from
.statusto.bottomBar.- Result: did not fix the disappearing toolbar.
-
Kept native toolbar, added a local
toolbarRefreshToken, updated it onhorizontalSizeClass/verticalSizeClasschanges, and attached.id(toolbarRefreshToken)to toolbar item contents.- Result: did not fix it.
-
Removed
.toolbarBackground(.hidden, for: .bottomBar).- Result: did not fix it.
-
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?