Repository navigation
Move the iOS tab bar to the side rail on iPhone Duo #275
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
1619e81
Move the iOS tab bar to the side rail on iPhone Duo
ninovanhooff 76582f7
Fix crash from an empty third tab on iPhone Duo
ninovanhooff 62eac5b
Apply the tab bar accent to the iPhone Duo side rail
ninovanhooff 116f60a
Make NativeTabBarController its own tab bar delegate
ninovanhooff ff46d2b
Merge branch 'main' into 274-ios-tab-bar-side-rail-iphone-duo
ninovanhooff File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 8 additions & 4 deletions
12
...ain/kotlin/nl/q42/template/core/ui/compose/composables/navigation/AppNavigationBar.ios.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
108 changes: 108 additions & 0 deletions
108
...n/kotlin/nl/q42/template/core/ui/compose/composables/navigation/NativeTabBarController.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| package nl.q42.template.core.ui.compose.composables.navigation | ||
|
|
||
| import kotlinx.cinterop.ExperimentalForeignApi | ||
| import nl.q42.template.core.ui.util.isLiquidGlassAvailable | ||
| import platform.UIKit.UIImage | ||
| import platform.UIKit.UITab | ||
| import platform.UIKit.UITabBarController | ||
| import platform.UIKit.UITabBarControllerDelegateProtocol | ||
| import platform.UIKit.UIViewAutoresizingFlexibleHeight | ||
| import platform.UIKit.UIViewAutoresizingFlexibleWidth | ||
| import platform.UIKit.UIViewController | ||
| import platform.UIKit.addChildViewController | ||
| import platform.UIKit.beginAppearanceTransition | ||
| import platform.UIKit.endAppearanceTransition | ||
| import platform.UIKit.didMoveToParentViewController | ||
|
|
||
| /** | ||
| * Root view controller on iOS 26+: a real UITabBarController, with the Compose view controller on top of its | ||
| * (empty) tabs. Only the bar of a UITabBarController follows the system placement: at the bottom, or on the side | ||
| * of the screen on iPhone Duo. A standalone UITabBar always stays at the bottom. | ||
| * | ||
| * Navigation stays in Compose, [NativeTabBar] sets the tabs and the selected tab. | ||
| * The Compose view controller never moves into a tab: Compose disposes everything when its view leaves the window. | ||
| */ | ||
| @OptIn(ExperimentalForeignApi::class) | ||
| internal class NativeTabBarController( | ||
| private val content: UIViewController, | ||
| ) : UITabBarController(nibName = null, bundle = null), UITabBarControllerDelegateProtocol { | ||
|
|
||
| internal var onItemSelected: (index: Int) -> Unit = {} | ||
| internal var onLayout: () -> Unit = {} | ||
|
|
||
| private var shownItems: List<AppNavigationBarItem>? = null | ||
|
|
||
| // Not in viewDidLoad: UITabBarController loads its view in its constructor, before content is set | ||
| init { | ||
| delegate = this | ||
| // Hidden until Compose shows a screen with tabs | ||
| setTabBarHidden(true) | ||
|
|
||
| addChildViewController(content) | ||
| // The tab bar controller view doesn't do Auto Layout for its subviews | ||
| content.view.setFrame(view.bounds) | ||
| content.view.autoresizingMask = UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight | ||
| view.addSubview(content.view) | ||
| content.didMoveToParentViewController(this) | ||
| } | ||
|
|
||
| override fun viewDidLayoutSubviews() { | ||
| super.viewDidLayoutSubviews() | ||
| keepContentAboveTabs() | ||
| onLayout() | ||
| } | ||
|
|
||
| /** Only recreates the tabs when they changed */ | ||
| internal fun updateItems(items: List<AppNavigationBarItem>) { | ||
| if (items == shownItems) return | ||
| shownItems = items | ||
| // UITab, not setViewControllers: that makes every child view controller a tab, Compose included | ||
| setTabs( | ||
| items.mapIndexed { index, item -> | ||
| UITab( | ||
| title = item.label, | ||
| image = UIImage.imageNamed(item.iosImageName), | ||
| identifier = index.toString(), | ||
| ) { UIViewController(nibName = null, bundle = null) } | ||
| }, | ||
| animated = false, | ||
| ) | ||
| keepContentAboveTabs() | ||
| } | ||
|
|
||
| internal fun selectItem(index: Int) { | ||
| val tab = tabs.getOrNull(index) as? UITab ?: return | ||
| if (selectedTab != tab) selectedTab = tab | ||
| } | ||
|
|
||
| /** | ||
| * The tabs are empty, Compose draws the content: keeps the Compose view controller a visible child, right above | ||
| * the tab views. The tab bar stays on top of Compose. | ||
| */ | ||
| internal fun keepContentAboveTabs() { | ||
| // setTabs removes every other child view controller, and tells it that it disappeared | ||
| if (content.parentViewController != this) { | ||
| addChildViewController(content) | ||
| content.didMoveToParentViewController(this) | ||
| if (content.view.window != null) { | ||
| content.beginAppearanceTransition(isAppearing = true, animated = false) | ||
| content.endAppearanceTransition() | ||
| } | ||
| } | ||
| val tabView = selectedViewController?.view ?: return | ||
| val tabContainer = generateSequence(tabView) { it.superview }.firstOrNull { it.superview == view } ?: return | ||
| if (view.subviews.indexOf(content.view) != view.subviews.indexOf(tabContainer) + 1) { | ||
| view.insertSubview(content.view, aboveSubview = tabContainer) | ||
| } | ||
| } | ||
|
ninovanhooff marked this conversation as resolved.
Resolved
|
||
|
|
||
| override fun tabBarController(tabBarController: UITabBarController, didSelectTab: UITab, previousTab: UITab?) { | ||
| // Selecting a tab swaps the (empty) tab views, which puts them on top of Compose | ||
| keepContentAboveTabs() | ||
| didSelectTab.identifier.toIntOrNull()?.let(onItemSelected) | ||
| } | ||
| } | ||
|
|
||
| /** Wraps the Compose view controller in a [NativeTabBarController] on iOS 26+, so [AppNavigationBar] gets Liquid Glass. */ | ||
| fun UIViewController.withNativeTabBarController(): UIViewController = | ||
| if (isLiquidGlassAvailable) NativeTabBarController(this) else this | ||
89 changes: 0 additions & 89 deletions
89
...kotlin/nl/q42/template/core/ui/compose/composables/navigation/TabBarLargeContentViewer.kt
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.