Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1748,7 +1748,7 @@ PODS:
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- SocketRocket
- react-native-bottom-tabs (1.0.5):
- react-native-bottom-tabs (1.1.0):
- boost
- DoubleConversion
- fast_float
Expand All @@ -1766,7 +1766,7 @@ PODS:
- React-graphics
- React-ImageManager
- React-jsi
- react-native-bottom-tabs/common (= 1.0.5)
- react-native-bottom-tabs/common (= 1.1.0)
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
Expand All @@ -1778,7 +1778,7 @@ PODS:
- SocketRocket
- SwiftUIIntrospect (~> 1.0)
- Yoga
- react-native-bottom-tabs/common (1.0.5):
- react-native-bottom-tabs/common (1.1.0):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2842,7 +2842,7 @@ SPEC CHECKSUMS:
React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48
React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696
React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b
react-native-bottom-tabs: 8e918142554e3878f043b23bdf93049b34a78ca6
react-native-bottom-tabs: e33312fc663d163f0be73d3474dfb448ba38dad8
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
Expand Down
19 changes: 13 additions & 6 deletions apps/example/src/Examples/FourTabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Article } from '../Screens/Article';
import { Albums } from '../Screens/Albums';
import { Contacts } from '../Screens/Contacts';
import { Chat } from '../Screens/Chat';
import type { ColorValue } from 'react-native';
import { Platform, type ColorValue } from 'react-native';

interface Props {
disablePageAnimations?: boolean;
Expand Down Expand Up @@ -48,22 +48,29 @@ export default function FourTabs({
badge: '5',
hidden: hideOneTab,
},

{
key: 'chat',
focusedIcon: require('../../assets/icons/chat_dark.png'),
title: 'Chat',
},
{
key: 'contacts',
focusedIcon: require('../../assets/icons/person_dark.png'),
title: 'Contacts',
badge: ' ',
},
{
key: 'chat',
focusedIcon: require('../../assets/icons/chat_dark.png'),
title: 'Chat',
role: 'search',
searchable: true,
navigationBarToolbarStyle:
Platform.Version === '26.0' ? 'hidden' : 'visible',
},
]);

return (
<TabView
onSearchFocusChange={(isFocused) => console.log('isFocused', isFocused)}
sidebarAdaptable
onSearchTextChange={(text) => console.log(text)}
disablePageAnimations={disablePageAnimations}
scrollEdgeAppearance={scrollEdgeAppearance}
navigationState={{ index, routes }}
Expand Down
30 changes: 22 additions & 8 deletions apps/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { Dimensions, Platform } from 'react-native';

const headerOptions = {
headerShown: true,
Expand Down Expand Up @@ -66,8 +67,12 @@ function ChatStackScreen() {
}

function NativeBottomTabsEmbeddedStacks() {
console.log(Platform.Version, 'Platform.Version');
return (
<Tab.Navigator sidebarAdaptable>
<Tab.Navigator
onSearchTextChange={(text) => console.log(text)}
onSearchFocusChange={(isFocused) => console.log('isFocused', isFocused)}
>
<Tab.Screen
name="Article"
component={ArticleStackScreen}
Expand All @@ -83,13 +88,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}
Expand All @@ -98,6 +97,21 @@ 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.0' &&
Platform.OS === 'ios' &&
Dimensions.get('window').width < 400
? 'hidden'
: 'visible',
}}
/>
</Tab.Navigator>
);
}
Expand Down
25 changes: 23 additions & 2 deletions packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand All @@ -210,7 +212,8 @@ - (void)updateState:(const facebook::react::State::Shared &)state oldState:(cons
}
}

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

- (void)onPageSelectedWithKey:(NSString *)key reactTag:(NSNumber *)reactTag {
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
Expand All @@ -221,6 +224,24 @@ - (void)onPageSelectedWithKey:(NSString *)key reactTag:(NSNumber *)reactTag {
}
}

- (void)onSearchFocusChangeWithIsFocused:(BOOL)isFocused reactTag:(NSNumber *)reactTag{
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
if (eventEmitter) {
eventEmitter->onSearchFocusChange(RNCTabViewEventEmitter::OnSearchFocusChange{
.isFocused = isFocused
});
}
}
- (void)onSearchTextChangeWithText:(NSString * _Nonnull)text reactTag:(NSNumber * _Nullable)reactTag {
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
if (eventEmitter) {
eventEmitter->onSearchTextChange(RNCTabViewEventEmitter::OnSearchTextChange{
.text = [text cStringUsingEncoding:NSUTF8StringEncoding]
});
}
}


- (void)onLongPressWithKey:(NSString *)key reactTag:(NSNumber *)reactTag {
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
if (eventEmitter) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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 makeUIViewController(context: Context) -> UIViewController {
let contentVC = UIViewController()
contentVC.view.backgroundColor = .clear
contentVC.view.addSubview(view)

return contentVC
}

#endif
}
38 changes: 32 additions & 6 deletions packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @State variable query and @FocusState variable focused are shared across all tabs in the TabView. This means when switching between tabs, the search query and focus state will persist across different tabs rather than being tab-specific. Each searchable tab should maintain its own search state.

Copilot uses AI. Check for mistakes.

@ViewBuilder
var body: some View {
Expand All @@ -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){
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 if keyword. Should be if (tabData.searchable) have a space as if (tabData.searchable) to match Swift formatting conventions (though Swift typically doesn't require parentheses around conditions).

Suggested change
if(tabData.searchable){
if tabData.searchable {

Copilot uses AI. Check for mistakes.
NavigationView{
//If it is not wrapped in UIViewController, it will crash.
Comment on lines +35 to +38
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.
RepresentableViewController(view: child.view)
.ignoresSafeArea(.container, edges: .all)
.tabAppear(using: context)
.hideTabBar(props.tabBarHidden)
.toolbar(tabData.navigationBarToolbarStyle.convert(), for: .navigationBar)

.searchFocused($focused)
.onChange(of: focused){ newValue in
onSearchFocusChange(newValue)
}
.onChange(of: query) { newValue in
onSearchTextChange(newValue)
}

}.navigationViewStyle(StackNavigationViewStyle())
.searchable(text: $query)
Comment on lines +44 to +54
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 .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.

Suggested change
.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 uses AI. Check for mistakes.
}else{
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 else keyword. Should format as } else { on a single line or properly indent the else block to match Swift formatting conventions.

Suggested change
}else{
} else {

Copilot uses AI. Check for mistakes.
RepresentableView(view: child.view)
.ignoresSafeArea(.container, edges: .all)
.tabAppear(using: context)
.hideTabBar(props.tabBarHidden)
}

} label: {
TabItem(
title: tabData.title,
Expand Down
8 changes: 6 additions & 2 deletions packages/react-native-bottom-tabs/ios/TabViewImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ struct TabViewImpl: View {
NewTabView(
props: props,
onLayout: onLayout,
onSelect: onSelect
onSelect: onSelect,
onSearchTextChange: onSearchTextChange,
onSearchFocusChange: onSearchFocusChange,
) {
#if !os(macOS)
updateTabBarAppearance(props: props, tabBar: tabBar)
Expand All @@ -36,11 +38,13 @@ struct TabViewImpl: View {
}
}
}

var onSelect: (_ key: String) -> Void
var onLongPress: (_ key: String) -> Void
var onLayout: (_ size: CGSize) -> Void
var onTabBarMeasured: (_ height: Int) -> Void
var onSearchTextChange: (_ text: String) -> Void
var onSearchFocusChange: (_ focused: Bool) -> Void

var body: some View {
tabContent
Expand Down
18 changes: 18 additions & 0 deletions packages/react-native-bottom-tabs/ios/TabViewProps.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ public enum TabBarRole: String {
}
}

public enum ToolbarStyle: String {
case automatic
case hidden
case visible

@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
func convert() -> Visibility {
switch self {
case .automatic:
return .automatic
case .hidden:
return .hidden
case .visible:
return .visible
}
}
}

struct IdentifiablePlatformView: Identifiable, Equatable {
let id = UUID()
let view: PlatformView
Expand Down
18 changes: 15 additions & 3 deletions packages/react-native-bottom-tabs/ios/TabViewProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public final class TabInfo: NSObject {
public let testID: String?
public let role: TabBarRole?
public let preventsDefault: Bool

public let searchable: Bool
public let navigationBarToolbarStyle: ToolbarStyle
public init(
key: String,
title: String,
Expand All @@ -23,7 +24,9 @@ public final class TabInfo: NSObject {
hidden: Bool,
testID: String?,
role: String?,
preventsDefault: Bool = false
preventsDefault: Bool = false,
searchable:Bool = false,
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 parameter declaration. Should be searchable: Bool = false for consistency with Swift formatting conventions.

Suggested change
searchable:Bool = false,
searchable: Bool = false,

Copilot uses AI. Check for mistakes.
navigationBarToolbarStyle: String? = "automatic"
) {
self.key = key
self.title = title
Expand All @@ -34,6 +37,8 @@ public final class TabInfo: NSObject {
self.testID = testID
self.role = TabBarRole(rawValue: role ?? "")
self.preventsDefault = preventsDefault
self.searchable = searchable
self.navigationBarToolbarStyle = ToolbarStyle(rawValue: navigationBarToolbarStyle ?? "automatic") ?? .automatic
super.init()
}
}
Expand All @@ -43,6 +48,8 @@ public final class TabInfo: NSObject {
func onLongPress(key: String, reactTag: NSNumber?)
func onTabBarMeasured(height: Int, reactTag: NSNumber?)
func onLayout(size: CGSize, reactTag: NSNumber?)
func onSearchTextChange(text: String, reactTag: NSNumber?)
func onSearchFocusChange(isFocused: Bool, reactTag: NSNumber?)
}

@objc public class TabViewProvider: PlatformView {
Expand All @@ -58,6 +65,8 @@ public final class TabInfo: NSObject {
@objc var onTabLongPress: RCTDirectEventBlock?
@objc var onTabBarMeasured: RCTDirectEventBlock?
@objc var onNativeLayout: RCTDirectEventBlock?
@objc var onSearchTextChange : RCTDirectEventBlock?
@objc var onSearchFocusChange : RCTDirectEventBlock?
Comment on lines +68 to +69
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.
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.

@objc public var icons: NSArray? {
didSet {
Expand Down Expand Up @@ -191,7 +200,6 @@ public final class TabInfo: NSObject {
if self.hostingController != nil {
return
}

self.hostingController = PlatformHostingController(rootView: TabViewImpl(props: props) { key in
self.delegate?.onPageSelected(key: key, reactTag: self.reactTag)
} onLongPress: { key in
Expand All @@ -200,6 +208,10 @@ public final class TabInfo: NSObject {
self.delegate?.onLayout(size: size, reactTag: self.reactTag)
} onTabBarMeasured: { height in
self.delegate?.onTabBarMeasured(height: height, reactTag: self.reactTag)
} onSearchTextChange : { text in
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 closure parameter. Should be onSearchTextChange : { text in have consistent spacing as onSearchTextChange: { text in to match Swift formatting conventions.

Suggested change
} onSearchTextChange : { text in
} onSearchTextChange: { text in

Copilot uses AI. Check for mistakes.
self.delegate?.onSearchTextChange(text: text, reactTag: self.reactTag)
} onSearchFocusChange: { isFocused in
self.delegate?.onSearchFocusChange(isFocused: isFocused, reactTag: self.rootTag)
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.

Using rootTag instead of reactTag parameter. This is inconsistent with other delegate methods and should use the reactTag parameter passed to the closure for consistency.

Suggested change
self.delegate?.onSearchFocusChange(isFocused: isFocused, reactTag: self.rootTag)
self.delegate?.onSearchFocusChange(isFocused: isFocused, reactTag: self.reactTag)

Copilot uses AI. Check for mistakes.
})

if let hostingController = self.hostingController, let parentViewController = reactViewController() {
Expand Down
Loading