Skip to content

Add bottom bar with Liquid Glass on iOS 26+ - #254

Open
ninovanhooff wants to merge 10 commits into
mainfrom
feature/cmp-201-liquid-glass-bottom-bar
Open

ninovanhooff wants to merge 10 commits into
mainfrom
feature/cmp-201-liquid-glass-bottom-bar

Conversation

@ninovanhooff

@ninovanhooff ninovanhooff commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Uses a native UITabBar on iOS 26+ so it gets Liquid Glass, and the Material NavigationBar on Android and older iOS versions. Navigation itself is unchanged: tabs navigate on the existing single back stack.

Why is this important?

One of our projects needs it

What is included

Simulator.Recording.-.iPhone.17.Pro.-.2026-10-20.at.14.53.43.mp4

Bottom bar (CMP-201)

  • New AppNavigationBar in core/ui. It only draws the bar: navigation stays the existing single back stack, with no tab containers.
    • Android and iOS < 26: Material NavigationBar (MaterialNavigationBar, with a Compose preview).
    • iOS 26+: native UITabBar (NativeTabBar), so it gets the Liquid Glass look. It's added through UIKitView with placedAsOverlay, so Compose positions it and measures its height.
    • Modifier: applied on every platform, including padding and position on iOS 26+.
    • Native tab bar behaviour: follows the app's dark mode and accent color, and updates its labels when the language changes. Native accessibility is on, so VoiceOver uses the native tab items.
  • BottomBarTab in shared defines the Home and Interop tabs.
    • NavigationRoot shows the bar only when a tab's screen is on top.
    • Switching tabs clears the back stack to Home.
  • Content scrolls behind the bar:
    • NavigationRoot provides the bar height as LocalAppNavigationBarHeight.
    • Screens add it as Scaffold bottomBar with AppNavigationBarSpacer, used in ScaffoldWithAppBar and SampleInteropScreen.
    • Snackbars stay above the bar.
  • New iOS utils in core/ui: isIosAtLeast() / isLiquidGlassAvailable and Color.toUIColor().
  • New resources: home_24 and extension_24 icons, and the navigation_tab_home / navigation_tab_interop strings.

Dutch translations

  • values-nl/strings.xml translates all strings except sample_lorem_ipsum.
  • Android: locales_config.xml and android:localeConfig, so the per-app language setting shows up (Android 13+).
  • iOS: CFBundleLocalizations (en, nl) in Info.plist, and nl in the Xcode project's knownRegions.

Sample app

  • Colored boxes and lorem ipsum on the sample home screen (SampleLoremIpsumBlocks), so there's content to see behind the glass.
  • The "Dummy content" title comes from the strings file (sample_dummy_content_title).
  • The platform greeting gets an (untranslated) prefix to show it doesn't follow the app language (SampleGetPlatformUserGreetingFlowUseCase).

Tested

  • iOS 27.1 simulator:
    • glass bar and tab switching
    • content behind the bar
    • bottom sheet drawn over the bar
    • live dark mode switch
    • Dutch
    • padded bar
  • Android emulator:
    • Material bar and tab switching
    • back to Home
    • bar hidden on Onboarding
    • live language switch

Not tested

  • VoiceOver / TalkBack
  • iOS versions below 26

Notes

Fixes #201

Uses a native UITabBar on iOS 26+ so it gets Liquid Glass, and the
Material NavigationBar on Android and older iOS versions. Navigation
itself is unchanged: tabs navigate on the existing single back stack.

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

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.

🔵 Needs a closer look

Native UIKit layout, accessibility, and live settings updates still require device or simulator validation.

1 open issue
What changed in this PR

Adds two-tab bottom navigation for issue #201, using native Liquid Glass on iOS 26+ and Material navigation elsewhere, while retaining a single back stack.

Changes:

  • Adds shared navigation-bar components and platform implementations.
  • Connects Home and Interop tabs to navigation and content padding.
  • Adds tab labels, icons, and UIKit helpers.
File Description
shared/​src/​commonMain/​kotlin/​nl/​q42/​template/​navigation/​NavigationRoot.kt Displays tabs and reserves bar space.
shared/​src/​commonMain/​kotlin/​nl/​q42/​template/​navigation/​BottomBarTab.kt Defines tabs and navigation behavior.
core/​ui/​src/​iosMain/​kotlin/​nl/​q42/​template/​core/​ui/​util/​UIColor.kt Converts Compose colors to UIKit colors.
core/​ui/​src/​iosMain/​kotlin/​nl/​q42/​template/​core/​ui/​util/​IsIosAtLeast.kt Checks native bar availability.
core/​ui/​src/​iosMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​NativeTabBar.kt Hosts and updates the native tab bar.
core/​ui/​src/​iosMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​AppNavigationBar.ios.kt Selects native or Material rendering.
core/​ui/​src/​commonMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​MaterialNavigationBar.kt Implements the Material bar.
core/​ui/​src/​commonMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​AppNavigationBarItem.kt Defines shared tab data.
core/​ui/​src/​commonMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​AppNavigationBar.kt Declares the cross-platform API.
core/​ui/​src/​commonMain/​composeResources/​values/​strings.xml Adds tab labels.
core/​ui/​src/​commonMain/​composeResources/​drawable/​home_24.xml Adds the Home icon.
core/​ui/​src/​commonMain/​composeResources/​drawable/​extension_24.xml Adds the Interop icon.
core/​ui/​src/​androidMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​AppNavigationBar.android.kt Uses Material rendering on Android.

Review effort: Balanced

ninovanhooff and others added 5 commits October 20, 2026 13:49
NavigationRoot no longer pads the content by the bar height. It provides
the height as LocalAppNavigationBarHeight instead, and screens add it as
Scaffold bottomBar via AppNavigationBarSpacer. Content now draws behind
the Liquid Glass bar while snackbars and FABs stay above it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gives the home screen scrollable content, so the Liquid Glass effect of
the bottom bar is visible.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds values-nl strings and declares nl on both platforms (Android
localeConfig, iOS CFBundleLocalizations), so the per-app language setting
shows up and language changes can be tested. Also moves the hardcoded
"Dummy content" title to the strings file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NativeTabBar now lays out a Compose placeholder with the caller's
modifier and pins the UITabBar to its bounds, so padding and positioning
work the same as on Android and iOS < 26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the manual placeholder syncing with UIKitView placed as an
overlay. Compose now applies the modifier, positions the tab bar and
measures its height with Auto Layout. The tab bar requests a remeasure
when its safe area changes, so the height includes the bottom inset.

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

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.

🔵 Needs a closer look

Padding-height fixes remain necessary, and native accessibility, live font-size changes, and older-iOS behavior need human validation.

1 open issue
1 previously missed issue

In code that hasn't changed since last review

Medium severity Navigation bar height excludes caller padding

core/​ui/​src/​commonMain/​kotlin/​nl/​q42/​template/​core/​ui/​compose/​composables/​navigation/​MaterialNavigationBar.kt:31

When the caller supplies bottom or vertical padding, onSizeChanged runs inside that padding and reports less than the bar's total occupied height. NavigationRoot uses this value for AppNavigationBarSpacer, so content, snackbars and floating action buttons can overlap the raised Material bar. Place the callback before the incoming modifier so the reported height includes its padding.

1 resolved since last review

Review effort: Balanced

Comment on lines +61 to +64
modifier = modifier
.fillMaxWidth()
.remeasureRequester(remeasureRequester)
.onSizeChanged { size -> onHeightChanged(with(density) { size.height.toDp() }) },

@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.

Some small comments, but nothing blocking IMO. Nice!

Comment thread core/ui/src/iosMain/kotlin/nl/q42/template/core/ui/util/UIColor.kt Outdated

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.

This seems like an unrelated change, maybe better suited for a separate PR?

Comment on lines +9 to +14
* @param iosSystemImageName SF Symbol name, used by the native iOS tab bar.
*/
data class AppNavigationBarItem(
val label: String,
val icon: DrawableResource,
val iosSystemImageName: String,

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.

The system image is indeed referring to a built-in SF Symbol. All of our project use custom images. So after generating a project from the template, developers will change this to iosImageName and add their own images to the asset catalogue. It's a small thing, but still.

ninovanhooff and others added 4 commits October 20, 2026 16:17
Tab bar labels don't grow with Dynamic Type. At the accessibility text
sizes a long press now shows the tab enlarged, like in a UITabBarController.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
And other PR feedback

Because that is what "real projects" eventually will use
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Add SwiftUI liquid glass bottom bar

3 participants