Skip to content

Conversation

@JorFlux
Copy link

@JorFlux JorFlux commented Dec 14, 2025

  • Added searchable boolean property to route options
  • Added navigationBarToolbarStyle property to control navigation bar toolbar visibility ('automatic' | 'hidden' | 'visible')
  • Added onSearchTextChange callback that fires when search text changes
  • Added onSearchFocusChange callback that fires when search field gains/loses focus
  • Created RepresentableViewController wrapper to properly integrate UIViewController with SwiftUI NavigationView (required for searchable functionality)
  • Updated iOS implementation to conditionally wrap searchable tabs in NavigationView and apply .searchable() modifier
  • Added support for both direct TabView usage and React Navigation integration

How to test?

  1. Using TabView directly:

    <TabView
      navigationState={{ index, routes }}
      onIndexChange={setIndex}
      renderScene={renderScene}
      onSearchTextChange={(text) => console.log('Search text:', text)}
      onSearchFocusChange={(isFocused) => console.log('Search focused:', isFocused)}
    />
    
    // In your routes:
    {
      key: 'contacts',
      title: 'Contacts',
      searchable: true,
      navigationBarToolbarStyle: 'visible', // or 'hidden' or 'automatic'
    }
  2. Using React Navigation:

    <Tab.Navigator
      onSearchTextChange={(text) => console.log('Search text:', text)}
      onSearchFocusChange={(isFocused) =>
        console.log('Search focused:', isFocused)
      }
    >
      <Tab.Screen
        name="Contacts"
        component={ContactsScreen}
        options={{
          searchable: true,
          navigationBarToolbarStyle: 'visible',
        }}
      />
    </Tab.Navigator>

Screenshots

For iOS 26 and higher
截圖 2025-12-14 下午4 03 27
For iOS 18
截圖 2025-12-14 下午4 04 53

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds search functionality to the native bottom tabs component for iOS 18+. It introduces searchable tab capabilities with customizable navigation bar toolbar visibility and provides callbacks for search text and focus changes. The implementation uses SwiftUI's .searchable() modifier wrapped in a NavigationView to integrate search functionality with React Native views.

Key Changes:

  • Added searchable and navigationBarToolbarStyle properties to tab route options
  • Implemented onSearchTextChange and onSearchFocusChange callbacks for search interactions
  • Created RepresentableViewController wrapper to bridge UIViewController with SwiftUI's NavigationView

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
packages/react-navigation/src/views/NativeBottomTabView.tsx Added getters for searchable and navigationBarToolbarStyle options from route descriptors
packages/react-navigation/src/types.ts Extended NativeBottomTabNavigationOptions with searchable and navigationBarToolbarStyle properties
packages/react-navigation/src/navigators/createNativeBottomTabNavigator.tsx Passed through search callbacks to NativeBottomTabView
packages/react-native-bottom-tabs/src/types.ts Added searchable and navigationBarToolbarStyle to BaseRoute type
packages/react-native-bottom-tabs/src/TabViewNativeComponent.ts Defined native event types and props for search functionality
packages/react-native-bottom-tabs/src/TabView.tsx Implemented search callbacks and added getters for searchable properties
packages/react-native-bottom-tabs/ios/TabViewProvider.swift Extended TabInfo with searchable properties and added delegate methods for search events
packages/react-native-bottom-tabs/ios/TabViewProps.swift Added ToolbarStyle enum with iOS 18+ Visibility conversion
packages/react-native-bottom-tabs/ios/TabViewImpl.swift Wired up search callbacks to native implementation
packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift Implemented searchable tabs with NavigationView wrapper and search modifiers
packages/react-native-bottom-tabs/ios/RepresentableViewController.swift Created UIViewController wrapper for SwiftUI integration (new file)
packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm Added native event emitters for search text and focus changes
apps/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx Added example usage with searchable Contacts tab
apps/example/src/Examples/FourTabs.tsx Added example usage with searchable Contacts tab
apps/example/ios/Podfile.lock Updated package version from 1.0.5 to 1.1.0

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +35 to +38
//Have to wrap in NavigationView to use searchable
if(tabData.searchable){
NavigationView{
//If it is not wrapped in UIViewController, it will crash.
Copy link

Copilot AI Dec 23, 2025

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.

Suggested change
//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 uses AI. Check for mistakes.

// MARK: TabViewProviderDelegate

// MARK: TabViewProviderDelegate
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Duplicate comment line with inconsistent formatting. The comment "// MARK: TabViewProviderDelegate" has extra spaces before "MARK:" compared to standard Swift conventions which typically use "// MARK:" with a single space.

Suggested change
// MARK: TabViewProviderDelegate
// MARK: TabViewProviderDelegate

Copilot uses AI. Check for mistakes.
role: 'search',
searchable: true,
navigationBarToolbarStyle:
Platform.Version === 26 || Platform.Version === '26.0'
Copy link

Copilot AI Dec 23, 2025

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.Version is always a string, so the numeric comparison === 26 will never be true. Only the string comparison is needed.

Suggested change
Platform.Version === 26 || Platform.Version === '26.0'
Platform.Version === '26.0'

Copilot uses AI. Check for mistakes.
Comment on lines +140 to 143
* Get navigation bar toolbar style for the tab, uses `route.navigationBarToolbarStyle` by default.
*/

/**
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Duplicate comment for getNavigationBarToolbarStyle. Lines 139-141 describe the same getter that's already documented in lines 109-114. One of these duplicate comment blocks should be removed.

Suggested change
* Get navigation bar toolbar style for the tab, uses `route.navigationBarToolbarStyle` by default.
*/
/**

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +43
export type OnChangeTextEventDataData = Readonly<{
text: string;
}>;
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

The type name OnChangeTextEventDataData has "Data" repeated twice. This should be renamed to OnChangeTextEventData or OnSearchTextChangeEventData for consistency with other event data types like OnPageSelectedEventData, OnSearchBarFocusChangeData, etc.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +40
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) {}
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

The makeUIView function is defined but never called for iOS builds. The protocol UIViewControllerRepresentable should only implement makeUIViewController and updateUIViewController on iOS. The makeUIView and updateUIView methods are for UIViewRepresentable, not UIViewControllerRepresentable.

Copilot uses AI. Check for mistakes.
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
}
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Missing implementation for macOS. The struct declares itself as UIViewControllerRepresentable which doesn't exist on macOS (should be NSViewControllerRepresentable). The macOS section implements view-related methods but the struct protocol is for view controllers.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +68 to +69
@objc var onSearchTextChange : RCTDirectEventBlock?
@objc var onSearchFocusChange : RCTDirectEventBlock?
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Missing space after colon in variable declaration. Should be onSearchTextChange : RCTDirectEventBlock? have consistent spacing as onSearchTextChange: RCTDirectEventBlock? to match the style of other properties in this file.

Suggested change
@objc var onSearchTextChange : RCTDirectEventBlock?
@objc var onSearchFocusChange : RCTDirectEventBlock?
@objc var onSearchTextChange: RCTDirectEventBlock?
@objc var onSearchFocusChange: RCTDirectEventBlock?

Copilot uses AI. Check for mistakes.
@objc var onTabBarMeasured: RCTDirectEventBlock?
@objc var onNativeLayout: RCTDirectEventBlock?
@objc var onSearchTextChange : RCTDirectEventBlock?
@objc var onSearchFocusChange : RCTDirectEventBlock?
Copy link

Copilot AI Dec 23, 2025

Choose a reason for hiding this comment

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

Missing space after colon in variable declaration. Should be onSearchFocusChange : RCTDirectEventBlock? have consistent spacing as onSearchFocusChange: RCTDirectEventBlock? to match the style of other properties in this file.

Suggested change
@objc var onSearchFocusChange : RCTDirectEventBlock?
@objc var onSearchFocusChange: RCTDirectEventBlock?

Copilot uses AI. Check for mistakes.
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
searchable: true,
navigationBarToolbarStyle:
Platform.Version === 26 || Platform.Version === '26.0'
Copy link

Copilot AI Dec 23, 2025

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.Version is always a string, so the numeric comparison === 26 will never be true. Only the string comparison is needed.

Suggested change
Platform.Version === 26 || Platform.Version === '26.0'
Platform.Version === '26.0'

Copilot uses AI. Check for mistakes.
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.

1 participant