-
Notifications
You must be signed in to change notification settings - Fork 47
feat: add Tab api support on newer OS #364
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
base: main
Are you sure you want to change the base?
Conversation
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
Adds support for the new SwiftUI Tab API on recent OS versions by splitting out a modern implementation and keeping a legacy fallback.
- Gate the new Tab API behind availability checks in
TabViewImpl
- Introduce
NewTabView
for iOS 18+/macOS 15+/visionOS 2+/tvOS 18+ - Extract existing logic into
LegacyTabView
and disable certain customizations pending future JS support
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
File | Description |
---|---|
packages/react-native-bottom-tabs/ios/TabViewImpl.swift | Refactors TabViewImpl to choose between NewTabView and LegacyTabView via a new tabContent property |
packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift | Implements the new SwiftUI Tab API with availability checks and placeholder badge/customization logic |
packages/react-native-bottom-tabs/ios/TabView/LegacyTabView.swift | Moves the original tab rendering logic into its own view for pre-18 OS versions |
Comments suppressed due to low confidence (1)
packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift:19
- The new
NewTabView
implementation introduces OS-gated behavior around selection, layout measurement, and appearance updates. Consider adding unit or UI tests to cover these code paths on both legacy and iOS 18+ configurations.
var body: some View {
labeled: props.labeled | ||
) | ||
} | ||
//.badge(tabData.badge) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The badge modifier is commented out due to empty badge strings producing a dot. To avoid this, conditionally apply .badge(tabData.badge)
only when tabData.badge
is non-empty, e.g.: if let badge = tabData.badge, !badge.isEmpty { view.badge(badge) }
.
//.badge(tabData.badge) | |
if let badge = tabData.badge, !badge.isEmpty { | |
.badge(badge) | |
} |
Copilot uses AI. Check for mistakes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this!
I've found some issues with current implementation that we need to sort out.
.measureView { size in | ||
onLayout(size) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you check if measureView returns the same values? Before we applied it to the ForEach, now it's applied to the TabView. I'm afraid this can cause issues and return different values
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hi, when testing i haven't had any problems with measureView applied to TabView, and found that it would always return one value/the same value as if measureView was applied to Tab child. also i tried moving it up to the ForEach, but it gave this error Referencing instance method 'measureView(onLayout:)' on 'ForEach' requires that 'some TabContent<String>' conform to 'View'
which i did not know how to resolve. Let me know if you think we should rework it and apply to the ForEach anyway, otherwise i'd leave it like this since it seems like nothing changes
labeled: props.labeled | ||
) | ||
} | ||
//.badge(tabData.badge) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why it's commented out?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as i wrote in the pr description, i commented it out because otherwise on iOS, even if tabData.badge is an empty string, a badge is created on the tab bar (see screenshot attached below). this behaviour can be seen on both ios 18 and 26, however it doesn't happen on ipad or other platforms. i tried using an if statement (like copilot suggested) but it broke the ForEach with Cannot convert value of type 'Range<Array<PlatformView>.Index>' (aka 'Range<Int>') to expected argument type 'Binding<C>'
. i also tried using view modifiers, moving that part to a separate functio, using tabData.badge.isEmpty ? nil : tabData.badge
(which does not work because .badge does not accept nil as value) and tabData.badge.isEmpty ? 0 : tabData.badge
(which does not work because a variable can't be of type Int or String), but nothing worked and it seems like the only way to remove it is either not call the function or call it with 0 (which can't be done since you either call it with a number or a string). let me know your thoughts on this and how we could resolve it, because honestly i have no idea
PR Description
This pr adds support for the new swift Tab api on ios/ipadOS/tvOS 18+, macOS 15+ and visionOS 2+. This let us implement many new features (such as search role for the tab item, tabbar customization, swipe actions and context menus), while also opening up an opportunity to implement #296 in the future.
In order to implement it i had to create a separate file for each implementation because only by doing so and using
@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
onNewTabView
i could then add@AppStorage("sidebarCustomizations")
. I tried to maintain every feature on each implementation and moved theTabViewImpl#renderTabItem
toLegacyTabView#renderTabItem
. i was hoping to add a similar function to the NewTabView struct but in that case the Tab api gave an error about not conforming to View (strange since the same code works directly in the ForEach).As of now i have disabled customization for each Tab (using
.customizationBehavior(.disabled, for: .sidebar, .tabBar)
), as we can then discuss in a followup pr/issue the js implementation, set Tab role to nil (same reason), disabled badges on the new tab (commented line) because even iftabData.badge
is an empty string, an empty red dot is created, and i could not figure out how to circumvent this. Finally, i found a strange behaviour with theNative Bottom Tabs with Custom Tab Bar
example, where for the new tab implementation, the native tab is still visible, but even there i could not figure out why.Let me know your thoughts and if you have any suggestion on how to resolve these issues
How to test?
Open the app and test the examples
Screenshots
N/A, the look is the same, only a new edit button has appeared in the ipad/macos sidebar