Skip to content

Commit 5bc4090

Browse files
committed
feat(ios): add searchable support for tabs
1 parent a66a108 commit 5bc4090

File tree

15 files changed

+256
-31
lines changed

15 files changed

+256
-31
lines changed

apps/example/ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,7 +1748,7 @@ PODS:
17481748
- React-RCTFBReactNativeSpec
17491749
- ReactCommon/turbomodule/core
17501750
- SocketRocket
1751-
- react-native-bottom-tabs (1.0.5):
1751+
- react-native-bottom-tabs (1.1.0):
17521752
- boost
17531753
- DoubleConversion
17541754
- fast_float
@@ -1766,7 +1766,7 @@ PODS:
17661766
- React-graphics
17671767
- React-ImageManager
17681768
- React-jsi
1769-
- react-native-bottom-tabs/common (= 1.0.5)
1769+
- react-native-bottom-tabs/common (= 1.1.0)
17701770
- React-NativeModulesApple
17711771
- React-RCTFabric
17721772
- React-renderercss
@@ -1778,7 +1778,7 @@ PODS:
17781778
- SocketRocket
17791779
- SwiftUIIntrospect (~> 1.0)
17801780
- Yoga
1781-
- react-native-bottom-tabs/common (1.0.5):
1781+
- react-native-bottom-tabs/common (1.1.0):
17821782
- boost
17831783
- DoubleConversion
17841784
- fast_float
@@ -2842,7 +2842,7 @@ SPEC CHECKSUMS:
28422842
React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48
28432843
React-Mapbuffer: 9d2434a42701d6144ca18f0ca1c4507808ca7696
28442844
React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b
2845-
react-native-bottom-tabs: 8e918142554e3878f043b23bdf93049b34a78ca6
2845+
react-native-bottom-tabs: e33312fc663d163f0be73d3474dfb448ba38dad8
28462846
react-native-safe-area-context: c6e2edd1c1da07bdce287fa9d9e60c5f7b514616
28472847
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
28482848
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d

apps/example/src/Examples/FourTabs.tsx

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Article } from '../Screens/Article';
44
import { Albums } from '../Screens/Albums';
55
import { Contacts } from '../Screens/Contacts';
66
import { Chat } from '../Screens/Chat';
7-
import type { ColorValue } from 'react-native';
7+
import { Platform, type ColorValue } from 'react-native';
88

99
interface Props {
1010
disablePageAnimations?: boolean;
@@ -48,22 +48,31 @@ export default function FourTabs({
4848
badge: '5',
4949
hidden: hideOneTab,
5050
},
51+
52+
{
53+
key: 'chat',
54+
focusedIcon: require('../../assets/icons/chat_dark.png'),
55+
title: 'Chat',
56+
},
5157
{
5258
key: 'contacts',
5359
focusedIcon: require('../../assets/icons/person_dark.png'),
5460
title: 'Contacts',
5561
badge: ' ',
56-
},
57-
{
58-
key: 'chat',
59-
focusedIcon: require('../../assets/icons/chat_dark.png'),
60-
title: 'Chat',
62+
role: 'search',
63+
searchable: true,
64+
navigationBarToolbarStyle:
65+
Platform.Version === 26 || Platform.Version === '26.0'
66+
? 'hidden'
67+
: 'visible',
6168
},
6269
]);
6370

6471
return (
6572
<TabView
73+
onSearchFocusChange={(isFocused) => console.log('isFocused', isFocused)}
6674
sidebarAdaptable
75+
onSearchTextChange={(text) => console.log(text)}
6776
disablePageAnimations={disablePageAnimations}
6877
scrollEdgeAppearance={scrollEdgeAppearance}
6978
navigationState={{ index, routes }}

apps/example/src/Examples/NativeBottomTabsEmbeddedStacks.tsx

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Contacts } from '../Screens/Contacts';
44
import { Chat } from '../Screens/Chat';
55
import { createNativeBottomTabNavigator } from '@bottom-tabs/react-navigation';
66
import { createNativeStackNavigator } from '@react-navigation/native-stack';
7+
import { Platform } from 'react-native';
78

89
const headerOptions = {
910
headerShown: true,
@@ -67,7 +68,10 @@ function ChatStackScreen() {
6768

6869
function NativeBottomTabsEmbeddedStacks() {
6970
return (
70-
<Tab.Navigator sidebarAdaptable>
71+
<Tab.Navigator
72+
onSearchTextChange={(text) => console.log(text)}
73+
onSearchFocusChange={(isFocused) => console.log('isFocused', isFocused)}
74+
>
7175
<Tab.Screen
7276
name="Article"
7377
component={ArticleStackScreen}
@@ -83,13 +87,7 @@ function NativeBottomTabsEmbeddedStacks() {
8387
tabBarIcon: () => require('../../assets/icons/grid_dark.png'),
8488
}}
8589
/>
86-
<Tab.Screen
87-
name="Contacts"
88-
component={ContactsStackScreen}
89-
options={{
90-
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
91-
}}
92-
/>
90+
9391
<Tab.Screen
9492
name="Chat"
9593
component={ChatStackScreen}
@@ -98,6 +96,19 @@ function NativeBottomTabsEmbeddedStacks() {
9896
require('../../assets/icons/message-circle-code.svg'),
9997
}}
10098
/>
99+
<Tab.Screen
100+
name="Contacts"
101+
component={ContactsStackScreen}
102+
options={{
103+
role: 'search',
104+
tabBarIcon: () => require('../../assets/icons/person_dark.png'),
105+
searchable: true,
106+
navigationBarToolbarStyle:
107+
Platform.Version === 26 || Platform.Version === '26.0'
108+
? 'hidden'
109+
: 'visible',
110+
}}
111+
/>
101112
</Tab.Navigator>
102113
);
103114
}

packages/react-native-bottom-tabs/ios/RCTTabViewComponentView.mm

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
192192
hidden:item.hidden
193193
testID:RCTNSStringFromStringNilIfEmpty(item.testID)
194194
role:RCTNSStringFromStringNilIfEmpty(item.role)
195-
preventsDefault:item.preventsDefault
195+
preventsDefault:item.preventsDefault
196+
searchable:item.searchable
197+
navigationBarToolbarStyle:RCTNSStringFromStringNilIfEmpty(item.navigationBarToolbarStyle)
196198
];
197199

198200
[result addObject:tabInfo];
@@ -210,7 +212,8 @@ - (void)updateState:(const facebook::react::State::Shared &)state oldState:(cons
210212
}
211213
}
212214

213-
// MARK: TabViewProviderDelegate
215+
216+
// MARK: TabViewProviderDelegate
214217

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

227+
- (void)onSearchFocusChangeWithIsFocused:(BOOL)isFocused reactTag:(NSNumber *)reactTag{
228+
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
229+
if (eventEmitter) {
230+
eventEmitter->onSearchFocusChange(RNCTabViewEventEmitter::OnSearchFocusChange{
231+
.isFocused = isFocused
232+
});
233+
}
234+
}
235+
- (void)onSearchTextChangeWithText:(NSString * _Nonnull)text reactTag:(NSNumber * _Nullable)reactTag {
236+
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
237+
if (eventEmitter) {
238+
eventEmitter->onSearchTextChange(RNCTabViewEventEmitter::OnSearchTextChange{
239+
.text = [text cStringUsingEncoding:NSUTF8StringEncoding]
240+
});
241+
}
242+
}
243+
244+
224245
- (void)onLongPressWithKey:(NSString *)key reactTag:(NSNumber *)reactTag {
225246
auto eventEmitter = std::static_pointer_cast<const RNCTabViewEventEmitter>(_eventEmitter);
226247
if (eventEmitter) {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import Foundation
2+
import SwiftUI
3+
4+
/**
5+
Helper used to render UIViewController inside of SwiftUI.
6+
This solves issues in some cases that can't found root UINavigationController.
7+
*/
8+
struct RepresentableViewController: UIViewControllerRepresentable {
9+
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
10+
11+
}
12+
13+
14+
var view: PlatformView
15+
16+
#if os(macOS)
17+
18+
func makeNSView(context: Context) -> PlatformView {
19+
let wrapper = NSView()
20+
wrapper.addSubview(view)
21+
return wrapper
22+
}
23+
24+
func updateNSView(_ nsView: PlatformView, context: Context) {}
25+
26+
#else
27+
28+
func makeUIView(context: Context) -> PlatformView {
29+
let wrapper = UIView()
30+
wrapper.addSubview(view)
31+
return wrapper
32+
}
33+
func makeUIViewController(context: Context) -> UIViewController {
34+
let contentVC = UIViewController()
35+
contentVC.view.backgroundColor = .clear
36+
contentVC.view.addSubview(view)
37+
38+
return contentVC
39+
}
40+
func updateUIView(_ uiView: PlatformView, context: Context) {}
41+
42+
#endif
43+
}

packages/react-native-bottom-tabs/ios/TabView/NewTabView.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import React
22
import SwiftUI
33

44
@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
5-
struct NewTabView: AnyTabView {
5+
struct NewTabView: AnyTabView {
66
@ObservedObject var props: TabViewProps
7-
87
var onLayout: (CGSize) -> Void
98
var onSelect: (String) -> Void
9+
var onSearchTextChange: ((String) -> Void)
10+
var onSearchFocusChange: ((Bool) -> Void)
1011
var updateTabBarAppearance: () -> Void
12+
@FocusState var focused: Bool
13+
@State var query = ""
1114

1215
@ViewBuilder
1316
var body: some View {
@@ -29,10 +32,31 @@ struct NewTabView: AnyTabView {
2932
)
3033

3134
Tab(value: tabData.key, role: tabData.role?.convert()) {
32-
RepresentableView(view: child.view)
33-
.ignoresSafeArea(.container, edges: .all)
34-
.tabAppear(using: context)
35-
.hideTabBar(props.tabBarHidden)
35+
//Have to wrap in NavigationView to use searchable
36+
if(tabData.searchable){
37+
NavigationView{
38+
//If it is not wrapped in UIViewController, it will crash.
39+
RepresentableViewController(view: child.view)
40+
.ignoresSafeArea(.container, edges: .all)
41+
.tabAppear(using: context)
42+
.hideTabBar(props.tabBarHidden)
43+
.toolbar(tabData.navigationBarToolbarStyle.convert(), for: .navigationBar)
44+
.searchable(text: $query)
45+
.searchFocused($focused)
46+
.onChange(of: focused){ newValue in
47+
onSearchFocusChange(newValue)
48+
}
49+
.onChange(of: query) { newValue in
50+
onSearchTextChange(newValue)
51+
}
52+
}
53+
}else{
54+
RepresentableView(view: child.view)
55+
.ignoresSafeArea(.container, edges: .all)
56+
.tabAppear(using: context)
57+
.hideTabBar(props.tabBarHidden)
58+
}
59+
3660
} label: {
3761
TabItem(
3862
title: tabData.title,

packages/react-native-bottom-tabs/ios/TabViewImpl.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ struct TabViewImpl: View {
1818
NewTabView(
1919
props: props,
2020
onLayout: onLayout,
21-
onSelect: onSelect
21+
onSelect: onSelect,
22+
onSearchTextChange: onSearchTextChange,
23+
onSearchFocusChange: onSearchFocusChange,
2224
) {
2325
#if !os(macOS)
2426
updateTabBarAppearance(props: props, tabBar: tabBar)
@@ -36,11 +38,13 @@ struct TabViewImpl: View {
3638
}
3739
}
3840
}
39-
41+
4042
var onSelect: (_ key: String) -> Void
4143
var onLongPress: (_ key: String) -> Void
4244
var onLayout: (_ size: CGSize) -> Void
4345
var onTabBarMeasured: (_ height: Int) -> Void
46+
var onSearchTextChange: (_ text: String) -> Void
47+
var onSearchFocusChange: (_ focused: Bool) -> Void
4448

4549
var body: some View {
4650
tabContent

packages/react-native-bottom-tabs/ios/TabViewProps.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ public enum TabBarRole: String {
3939
}
4040
}
4141

42+
public enum ToolbarStyle: String {
43+
case automatic
44+
case hidden
45+
case visible
46+
47+
@available(iOS 18, macOS 15, visionOS 2, tvOS 18, *)
48+
func convert() -> Visibility {
49+
switch self {
50+
case .automatic:
51+
return .automatic
52+
case .hidden:
53+
return .hidden
54+
case .visible:
55+
return .visible
56+
}
57+
}
58+
}
59+
4260
struct IdentifiablePlatformView: Identifiable, Equatable {
4361
let id = UUID()
4462
let view: PlatformView

packages/react-native-bottom-tabs/ios/TabViewProvider.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ public final class TabInfo: NSObject {
1313
public let testID: String?
1414
public let role: TabBarRole?
1515
public let preventsDefault: Bool
16-
16+
public let searchable: Bool
17+
public let navigationBarToolbarStyle: ToolbarStyle
1718
public init(
1819
key: String,
1920
title: String,
@@ -23,7 +24,9 @@ public final class TabInfo: NSObject {
2324
hidden: Bool,
2425
testID: String?,
2526
role: String?,
26-
preventsDefault: Bool = false
27+
preventsDefault: Bool = false,
28+
searchable:Bool = false,
29+
navigationBarToolbarStyle: String? = "automatic"
2730
) {
2831
self.key = key
2932
self.title = title
@@ -34,6 +37,8 @@ public final class TabInfo: NSObject {
3437
self.testID = testID
3538
self.role = TabBarRole(rawValue: role ?? "")
3639
self.preventsDefault = preventsDefault
40+
self.searchable = searchable
41+
self.navigationBarToolbarStyle = ToolbarStyle(rawValue: navigationBarToolbarStyle ?? "automatic") ?? .automatic
3742
super.init()
3843
}
3944
}
@@ -43,6 +48,8 @@ public final class TabInfo: NSObject {
4348
func onLongPress(key: String, reactTag: NSNumber?)
4449
func onTabBarMeasured(height: Int, reactTag: NSNumber?)
4550
func onLayout(size: CGSize, reactTag: NSNumber?)
51+
func onSearchTextChange(text: String, reactTag: NSNumber?)
52+
func onSearchFocusChange(isFocused: Bool, reactTag: NSNumber?)
4653
}
4754

4855
@objc public class TabViewProvider: PlatformView {
@@ -58,6 +65,8 @@ public final class TabInfo: NSObject {
5865
@objc var onTabLongPress: RCTDirectEventBlock?
5966
@objc var onTabBarMeasured: RCTDirectEventBlock?
6067
@objc var onNativeLayout: RCTDirectEventBlock?
68+
@objc var onSearchTextChange : RCTDirectEventBlock?
69+
@objc var onSearchFocusChange : RCTDirectEventBlock?
6170

6271
@objc public var icons: NSArray? {
6372
didSet {
@@ -191,7 +200,6 @@ public final class TabInfo: NSObject {
191200
if self.hostingController != nil {
192201
return
193202
}
194-
195203
self.hostingController = PlatformHostingController(rootView: TabViewImpl(props: props) { key in
196204
self.delegate?.onPageSelected(key: key, reactTag: self.reactTag)
197205
} onLongPress: { key in
@@ -200,6 +208,10 @@ public final class TabInfo: NSObject {
200208
self.delegate?.onLayout(size: size, reactTag: self.reactTag)
201209
} onTabBarMeasured: { height in
202210
self.delegate?.onTabBarMeasured(height: height, reactTag: self.reactTag)
211+
} onSearchTextChange : { text in
212+
self.delegate?.onSearchTextChange(text: text, reactTag: self.reactTag)
213+
} onSearchFocusChange: { isFocused in
214+
self.delegate?.onSearchFocusChange(isFocused: isFocused, reactTag: self.rootTag)
203215
})
204216

205217
if let hostingController = self.hostingController, let parentViewController = reactViewController() {

0 commit comments

Comments
 (0)