-
Notifications
You must be signed in to change notification settings - Fork 82
feat: enhance bottom tabs with search functionality #493
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { Contacts } from '../Screens/Contacts'; | |||||
| import { Chat } from '../Screens/Chat'; | ||||||
| import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation'; | ||||||
| import { createNativeStackNavigator } from '@react-navigation/native-stack'; | ||||||
| import { Platform } from 'react-native'; | ||||||
|
|
||||||
| const headerOptions = { | ||||||
| headerShown: true, | ||||||
|
|
@@ -67,7 +68,10 @@ function ChatStackScreen() { | |||||
|
|
||||||
| function NativeBottomTabsEmbeddedStacks() { | ||||||
| return ( | ||||||
| <Tab.Navigator sidebarAdaptable> | ||||||
| <Tab.Navigator | ||||||
| onSearchTextChange={(text) => console.log(text)} | ||||||
| onSearchFocusChange={(isFocused) => console.log('isFocused', isFocused)} | ||||||
| > | ||||||
| <Tab.Screen | ||||||
| name="Article" | ||||||
| component={ArticleStackScreen} | ||||||
|
|
@@ -83,13 +87,7 @@ function NativeBottomTabsEmbeddedStacks() { | |||||
| tabBarIcon: () => require('../../assets/icons/grid_dark.png'), | ||||||
| }} | ||||||
| /> | ||||||
| <Tab.Screen | ||||||
| name="Contacts" | ||||||
| component={ContactsStackScreen} | ||||||
| options={{ | ||||||
| tabBarIcon: () => require('../../assets/icons/person_dark.png'), | ||||||
| }} | ||||||
| /> | ||||||
|
|
||||||
| <Tab.Screen | ||||||
| name="Chat" | ||||||
| component={ChatStackScreen} | ||||||
|
|
@@ -98,6 +96,19 @@ function NativeBottomTabsEmbeddedStacks() { | |||||
| require('../../assets/icons/message-circle-code.svg'), | ||||||
| }} | ||||||
| /> | ||||||
| <Tab.Screen | ||||||
| name="Contacts" | ||||||
| component={ContactsStackScreen} | ||||||
| options={{ | ||||||
| role: 'search', | ||||||
| tabBarIcon: () => require('../../assets/icons/person_dark.png'), | ||||||
| searchable: true, | ||||||
| navigationBarToolbarStyle: | ||||||
| Platform.Version === 26 || Platform.Version === '26.0' | ||||||
|
||||||
| Platform.Version === 26 || Platform.Version === '26.0' | |
| Platform.Version === '26.0' |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -192,7 +192,9 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const & | |||||
| hidden:item.hidden | ||||||
| testID:RCTNSStringFromStringNilIfEmpty(item.testID) | ||||||
| role:RCTNSStringFromStringNilIfEmpty(item.role) | ||||||
| preventsDefault:item.preventsDefault | ||||||
| preventsDefault:item.preventsDefault | ||||||
| searchable:item.searchable | ||||||
| navigationBarToolbarStyle:RCTNSStringFromStringNilIfEmpty(item.navigationBarToolbarStyle) | ||||||
| ]; | ||||||
|
|
||||||
| [result addObject:tabInfo]; | ||||||
|
|
@@ -210,7 +212,8 @@ - (void)updateState:(const facebook::react::State::Shared &)state oldState:(cons | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| // MARK: TabViewProviderDelegate | ||||||
|
|
||||||
| // MARK: TabViewProviderDelegate | ||||||
|
||||||
| // MARK: TabViewProviderDelegate | |
| // MARK: TabViewProviderDelegate |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import SwiftUI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Helper used to render UIViewController inside of SwiftUI. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This solves issues in some cases that can't found root UINavigationController. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct RepresentableViewController: UIViewControllerRepresentable { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func updateUIViewController(_ uiViewController: UIViewController, context: Context) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var view: PlatformView | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #if os(macOS) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func makeNSView(context: Context) -> PlatformView { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let wrapper = NSView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wrapper.addSubview(view) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return wrapper | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func updateNSView(_ nsView: PlatformView, context: Context) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #else | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func makeUIView(context: Context) -> PlatformView { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let wrapper = UIView() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| wrapper.addSubview(view) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return wrapper | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func makeUIViewController(context: Context) -> UIViewController { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let contentVC = UIViewController() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentVC.view.backgroundColor = .clear | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentVC.view.addSubview(view) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return contentVC | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func updateUIView(_ uiView: PlatformView, context: Context) {} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+28
to
+40
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #endif | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+43
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| struct RepresentableViewController: UIViewControllerRepresentable { | |
| func updateUIViewController(_ uiViewController: UIViewController, context: Context) { | |
| } | |
| var view: PlatformView | |
| #if os(macOS) | |
| func makeNSView(context: Context) -> PlatformView { | |
| let wrapper = NSView() | |
| wrapper.addSubview(view) | |
| return wrapper | |
| } | |
| func updateNSView(_ nsView: PlatformView, context: Context) {} | |
| #else | |
| func makeUIView(context: Context) -> PlatformView { | |
| let wrapper = UIView() | |
| wrapper.addSubview(view) | |
| return wrapper | |
| } | |
| func makeUIViewController(context: Context) -> UIViewController { | |
| let contentVC = UIViewController() | |
| contentVC.view.backgroundColor = .clear | |
| contentVC.view.addSubview(view) | |
| return contentVC | |
| } | |
| func updateUIView(_ uiView: PlatformView, context: Context) {} | |
| #endif | |
| } | |
| #if os(macOS) | |
| struct RepresentableViewController: NSViewRepresentable { | |
| var view: PlatformView | |
| func makeNSView(context: Context) -> PlatformView { | |
| let wrapper = NSView() | |
| wrapper.addSubview(view) | |
| return wrapper | |
| } | |
| func updateNSView(_ nsView: PlatformView, context: Context) {} | |
| } | |
| #else | |
| struct RepresentableViewController: UIViewControllerRepresentable { | |
| var view: PlatformView | |
| func makeUIView(context: Context) -> PlatformView { | |
| let wrapper = UIView() | |
| wrapper.addSubview(view) | |
| return wrapper | |
| } | |
| func updateUIView(_ uiView: PlatformView, context: Context) {} | |
| func makeUIViewController(context: Context) -> UIViewController { | |
| let contentVC = UIViewController() | |
| contentVC.view.backgroundColor = .clear | |
| contentVC.view.addSubview(view) | |
| return contentVC | |
| } | |
| func updateUIViewController(_ uiViewController: UIViewController, context: Context) { | |
| } | |
| } | |
| #endif |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,12 +2,15 @@ import React | |||||||||||||||||||||||||||||||||||||||||||||
| import SwiftUI | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @available(iOS 18, macOS 15, visionOS 2, tvOS 18, *) | ||||||||||||||||||||||||||||||||||||||||||||||
| struct NewTabView: AnyTabView { | ||||||||||||||||||||||||||||||||||||||||||||||
| struct NewTabView: AnyTabView { | ||||||||||||||||||||||||||||||||||||||||||||||
| @ObservedObject var props: TabViewProps | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| var onLayout: (CGSize) -> Void | ||||||||||||||||||||||||||||||||||||||||||||||
| var onSelect: (String) -> Void | ||||||||||||||||||||||||||||||||||||||||||||||
| var onSearchTextChange: ((String) -> Void) | ||||||||||||||||||||||||||||||||||||||||||||||
| var onSearchFocusChange: ((Bool) -> Void) | ||||||||||||||||||||||||||||||||||||||||||||||
| var updateTabBarAppearance: () -> Void | ||||||||||||||||||||||||||||||||||||||||||||||
| @FocusState var focused: Bool | ||||||||||||||||||||||||||||||||||||||||||||||
| @State var query = "" | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+12
to
+13
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| @ViewBuilder | ||||||||||||||||||||||||||||||||||||||||||||||
| var body: some View { | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -29,10 +32,33 @@ struct NewTabView: AnyTabView { | |||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Tab(value: tabData.key, role: tabData.role?.convert()) { | ||||||||||||||||||||||||||||||||||||||||||||||
| RepresentableView(view: child.view) | ||||||||||||||||||||||||||||||||||||||||||||||
| .ignoresSafeArea(.container, edges: .all) | ||||||||||||||||||||||||||||||||||||||||||||||
| .tabAppear(using: context) | ||||||||||||||||||||||||||||||||||||||||||||||
| .hideTabBar(props.tabBarHidden) | ||||||||||||||||||||||||||||||||||||||||||||||
| //Have to wrap in NavigationView to use searchable | ||||||||||||||||||||||||||||||||||||||||||||||
| if(tabData.searchable){ | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| if(tabData.searchable){ | |
| if tabData.searchable { |
Copilot
AI
Dec 23, 2025
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 comment states "Have to wrap in NavigationView to use searchable" but doesn't explain why wrapping in UIViewController is required. The comment on line 38 mentions it will crash but doesn't provide context about what causes the crash or under what conditions.
| //Have to wrap in NavigationView to use searchable | |
| if(tabData.searchable){ | |
| NavigationView{ | |
| //If it is not wrapped in UIViewController, it will crash. | |
| // `.searchable` renders the search field in the navigation bar, so the content | |
| // must be placed inside a `NavigationView` for the search UI to appear correctly. | |
| if(tabData.searchable){ | |
| NavigationView{ | |
| // The React Native root view is a UIKit `UIView`. When used as the root content | |
| // of a `NavigationView` with `.searchable`, embedding the `UIView` directly | |
| // (without wrapping it in a `UIViewController`) causes a runtime crash on iOS. | |
| // `RepresentableViewController` wraps the `UIView` in a `UIViewController` | |
| // to satisfy SwiftUI's expectations for the navigation/search container. |
Copilot
AI
Dec 23, 2025
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 .searchFocused($focused) modifier is applied to RepresentableViewController but should be applied to the NavigationView or a level that encompasses the searchable content. The current placement may not work as expected with the search functionality.
| .searchFocused($focused) | |
| .onChange(of: focused){ newValue in | |
| onSearchFocusChange(newValue) | |
| } | |
| .onChange(of: query) { newValue in | |
| onSearchTextChange(newValue) | |
| } | |
| }.navigationViewStyle(StackNavigationViewStyle()) | |
| .searchable(text: $query) | |
| } | |
| .navigationViewStyle(StackNavigationViewStyle()) | |
| .searchable(text: $query) | |
| .searchFocused($focused) | |
| .onChange(of: focused){ newValue in | |
| onSearchFocusChange(newValue) | |
| } | |
| .onChange(of: query) { newValue in | |
| onSearchTextChange(newValue) | |
| } |
Copilot
AI
Dec 23, 2025
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.
Missing space after else keyword. Should format as } else { on a single line or properly indent the else block to match Swift formatting conventions.
| }else{ | |
| } else { |
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 version check uses
Platform.Version === 26 || Platform.Version === '26.0'which checks for both number and string. On iOS,Platform.Versionis always a string, so the numeric comparison=== 26will never be true. Only the string comparison is needed.