Skip to content

Move the iOS tab bar to the side rail on iPhone Duo - #275

Merged
ninovanhooff merged 5 commits into
mainfrom
274-ios-tab-bar-side-rail-iphone-duo
Nov 4, 2026
Merged

ninovanhooff merged 5 commits into
mainfrom
274-ios-tab-bar-side-rail-iphone-duo

Conversation

@ninovanhooff

@ninovanhooff ninovanhooff commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #274

Why is this important?

On iPhone Duo the system puts tab bars in a vertical rail on the side of the screen, to leave more height for content. Only the inner display in portrait keeps the tab bar at the bottom:

video

rotation.mp4

poses

Pose Tab bar
Closed (outer display), portrait or landscape Side rail
Open, one app, portrait Bottom
Open, one app, landscape Side rail
Open, two apps side by side Side rail, on each app's outer edge

The issue described the open state as "bottom" without mentioning orientation. In landscape the open Duo uses the side rail too. Sources: the HIG on vertical controls, and Apple's iPhone Duo developer forums summary, which says the inner display in portrait "maintain[s] standard horizontal bars", while in landscape and Split View the "controls move to the side edge".
I also tested this myself using the iphone duo at the rotterdam office

The system decides the placement, so the app needs no code per pose. But our Liquid Glass tab bar was a standalone UITabBar, which always stays at the bottom. Only the bar of a UITabBarController follows the system placement.

Notes

  • On iOS 26+ the root view controller is now a UITabBarController (NativeTabBarController) with empty tabs and the Compose view controller on top. Navigation stays in Compose; NativeTabBar only sets the tabs and the selected tab.
  • The tabs use the UITab API, not setViewControllers: with setViewControllers every child view controller is a tab, so Compose became an empty third tab, and selecting it crashed the app.
  • The Compose view controller never moves into a tab, because Compose disposes everything when its view leaves the window. setTabs detaches it once, so it is re-attached and told it appeared again.
  • A vertical bar reports a height of 0, so screens don't add bottom padding for it.
  • The accent color is set on the controller's view, not on the tab bar: the Duo side rail is not part of the tab bar.
  • The custom Large Content Viewer is removed: UITabBarController provides it.
  • iOS < 26 and Android still use the Material bar.

Tested in the simulator (iphone duo + iphone 17) and real iphone duo:

  • iPhone Duo closed: bar on the side rail, switching tabs, Back, dark mode and larger text.
  • iPhone Duo open, one app: bar at the bottom in portrait and on the side rail in landscape, with the app accent; switching tabs works in both.
  • iPhone 17 Pro: bar at the bottom with the app accent, bottom padding, Onboarding hides the bar and back brings it back.
  • VoiceOver (accessibility tree dump, before the UITab change): both the Compose content and the tabs are reachable, on both devices.
  • VoiceOver: tabs can be sselected in both side and bottom positions
  • Large Content Viewer
  • iPhone Duo with two apps side by side

🤖 Generated with Claude Code

ninovanhooff and others added 2 commits October 29, 2026 11:02
On iOS 26+ the app root is now a real UITabBarController with empty tabs and
the Compose view controller on top. Only the bar of a UITabBarController
follows the system placement, so it moves to the side rail on iPhone Duo and
stays at the bottom elsewhere. Navigation stays in Compose.

A vertical bar reports a height of 0, so screens don't pad for it. The custom
Large Content Viewer is removed, UITabBarController provides it.

Fixes #274

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UITabBarController treats every child view controller as a tab, so the
Compose view controller became an empty third tab. Selecting it passed
index 2 to Compose, which only has two tabs, and crashed.

Declare the tabs with UITab instead, so the Compose view controller stays a
child without becoming a tab, and map selection by tab identifier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 29, 2026 10:14
@ninovanhooff
ninovanhooff requested a review from a team as a code owner October 29, 2026 10:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The side rail still falls back to system blue because the accent tint is applied only to the bottom tab bar, not the controller’s root view.

1 open issue
What changed in this PR

Moves iOS 26+ navigation into a native UITabBarController so UIKit can place tabs on the iPhone Duo side rail while navigation remains managed by Compose.

Changes:

  • Wraps the Compose controller with native tab management and content-layer handling.
  • Updates tab selection, visibility, and bottom spacing for adaptive placement.
  • Removes the custom Large Content Viewer in favor of UIKit’s built-in support; retains Material navigation on older iOS versions and Android.
File Description
shared/​src/​iosMain/​kotlin/​main.kt Wraps the Compose entry point in the native tab controller.
core/​ui/​src/​iosMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​TabBarLargeContentViewer.kt Removes custom Large Content Viewer support.
core/​ui/​src/​iosMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​NativeTabBarController.kt Adds adaptive native tabs while preserving Compose content.
core/​ui/​src/​iosMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​NativeTabBar.kt Connects Compose state to native tabs and reports bottom spacing.
core/​ui/​src/​iosMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​AppNavigationBar.ios.kt Uses the native controller when available, otherwise Material.
core/​ui/​src/​commonMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​AppNavigationBar.kt Documents adaptive placement and side-rail height reporting.

Review effort: Balanced

The side rail is not part of controller.tabBar, so it fell back to system
blue. Set the tint on the controller's view so both placements inherit it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@mbernson mbernson left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One change request for the tab bar delegate. Looks good other than that :)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@AnneKlapwijk AnneKlapwijk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks clear. Tested https://github.com/Q42/GoingsOn.CMP/pull/47 on iPhone Duo simulator. Looks good 👍

@ninovanhooff
ninovanhooff requested review from mbernson and removed request for mbernson November 3, 2026 16:49
@ninovanhooff
ninovanhooff merged commit 8994e1e into main Nov 4, 2026
3 checks passed
@ninovanhooff
ninovanhooff deleted the 274-ios-tab-bar-side-rail-iphone-duo branch November 4, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Move Tab bar to the side rail on Iphone Duo

4 participants