Skip to content

Commit 4a383a9

Browse files
authored
fix(iOS): allow to hide the search bar (#2926)
## Description Search bar on iOS would not disappear after setting `headerSearchBarOptions` to `undefined`. It seems like this issue was present [for some time now](#758 (comment)) but somehow it wasn't reported in a seperate issue. ## Changes - set `navItem.searchController` to `nil` if there is no `RNSScreenStackHeaderSubviewTypeSearchBar` anymore - add `Test2926` test screen and e2e test ## Test code and steps to reproduce Open `Test2926` in Example app, try to hide the search bar. ## Checklist - [x] Included code example that can be used to test this change - [x] Ensured that CI passes
1 parent b673a0c commit 4a383a9

4 files changed

Lines changed: 153 additions & 8 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { device, expect, element, by } from 'detox';
2+
import { describeIfiOS } from '../e2e-utils';
3+
4+
// PR related to iOS search bar
5+
describeIfiOS('Test2926', () => {
6+
beforeAll(async () => {
7+
await device.reloadReactNative();
8+
});
9+
10+
it('Test2926 should exist', async () => {
11+
await waitFor(element(by.id('root-screen-tests-Test2926')))
12+
.toBeVisible()
13+
.whileElement(by.id('root-screen-examples-scrollview'))
14+
.scroll(600, 'down', NaN, 0.85);
15+
16+
await expect(element(by.id('root-screen-tests-Test2926'))).toBeVisible();
17+
await element(by.id('root-screen-tests-Test2926')).tap();
18+
});
19+
20+
it('searchBar should be initially visible', async () => {
21+
await expect(element(by.type('UISearchBarTextField'))).toBeVisible();
22+
});
23+
24+
it('searchBar should hide after setting headerSearchBarOptions to undefined', async () => {
25+
await element(by.id('home-switch-search-enabled')).tap();
26+
await expect(element(by.type('UISearchBarTextField'))).not.toBeVisible();
27+
});
28+
29+
it('searchBar value should stay in text field after coming back from another screen', async () => {
30+
await element(by.id('home-switch-search-enabled')).tap();
31+
32+
await element(by.type('UISearchBarTextField')).replaceText('Item 2');
33+
await element(by.id('home-button-open-second')).tap();
34+
35+
await element(by.id('BackButton')).tap();
36+
37+
await expect(element(by.type('UISearchBarTextField'))).toBeVisible();
38+
await expect(element(by.type('UISearchBarTextField'))).toHaveText('Item 2');
39+
});
40+
});

apps/src/tests/Test2926.tsx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import React from 'react';
2+
import { NavigationContainer, ParamListBase } from '@react-navigation/native';
3+
import {
4+
NativeStackNavigationProp,
5+
createNativeStackNavigator,
6+
} from '@react-navigation/native-stack';
7+
import { Button, Text, View, ScrollView } from 'react-native';
8+
import {
9+
ListItem,
10+
SettingsSwitch,
11+
} from '../shared';
12+
13+
type StackRouteParamList = {
14+
Home: undefined;
15+
Second: undefined;
16+
};
17+
18+
type NavigationProp<ParamList extends ParamListBase> = {
19+
navigation: NativeStackNavigationProp<ParamList>;
20+
};
21+
22+
type StackNavigationProp = NavigationProp<StackRouteParamList>;
23+
24+
const Stack = createNativeStackNavigator<StackRouteParamList>();
25+
26+
const items = Array.from({ length: 40 }, (_, i) => `Item ${i + 1}`);
27+
28+
function Second({}: StackNavigationProp) {
29+
return (
30+
<View>
31+
<Text>Second screen</Text>
32+
</View>
33+
);
34+
}
35+
36+
function Home({ navigation }: StackNavigationProp) {
37+
const [searchEnabled, setSearchEnabled] = React.useState(true);
38+
const [searchQuery, setSearchQuery] = React.useState('');
39+
40+
React.useLayoutEffect(() => {
41+
if (searchEnabled) {
42+
navigation.setOptions({
43+
headerSearchBarOptions: {
44+
onChangeText: event => setSearchQuery(event.nativeEvent.text),
45+
},
46+
});
47+
} else {
48+
setSearchQuery('');
49+
navigation.setOptions({
50+
headerSearchBarOptions: undefined,
51+
});
52+
}
53+
}, [navigation, searchEnabled]);
54+
55+
return (
56+
<View style={[{ flex: 1, gap: 15 }]}>
57+
<ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={{ marginTop: 15}}>
58+
<Button
59+
title="Open Second"
60+
onPress={() => navigation.navigate('Second')}
61+
testID="home-button-open-second"
62+
/>
63+
<SettingsSwitch
64+
style={{ marginTop: 15, marginBottom: 15 }}
65+
label="Search enabled"
66+
value={searchEnabled}
67+
onValueChange={() => setSearchEnabled(!searchEnabled)}
68+
testID="home-switch-search-enabled"
69+
/>
70+
{items
71+
.filter(
72+
name =>
73+
searchQuery === '' ||
74+
name.toLowerCase().includes(searchQuery.toLowerCase()),
75+
)
76+
.map(name => (
77+
<ListItem key={name} title={name} onPress={() => {}} />
78+
))}
79+
</ScrollView>
80+
</View>
81+
);
82+
}
83+
84+
export default function App() {
85+
return (
86+
<NavigationContainer>
87+
<Stack.Navigator>
88+
<Stack.Screen name="Home" component={Home} />
89+
<Stack.Screen name="Second" component={Second} />
90+
</Stack.Navigator>
91+
</NavigationContainer>
92+
);
93+
}

apps/src/tests/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export { default as Test2855 } from './Test2855';
136136
export { default as Test2877 } from './Test2877'; // [E2E created](iOS): issue is related to formSheet on iOS
137137
export { default as Test2895 } from './Test2895';
138138
export { default as Test2899 } from './Test2899';
139+
export { default as Test2926 } from './Test2926'; // [E2E created](iOS): PR related to iOS search bar
139140
export { default as TestScreenAnimation } from './TestScreenAnimation';
140141
export { default as TestScreenAnimationV5 } from './TestScreenAnimationV5';
141142
export { default as TestHeader } from './TestHeader';

ios/RNSScreenStackHeaderConfig.mm

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,12 @@ + (void)updateViewController:(UIViewController *)vc
615615
navitem.rightBarButtonItem = nil;
616616
navitem.titleView = nil;
617617

618+
#if !TARGET_OS_TV
619+
// We want to set navitem.searchController to nil only if we are sure
620+
// that we are removing the search bar from the header.
621+
bool searchBarPresent = false;
622+
#endif /* !TARGET_OS_TV */
623+
618624
for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) {
619625
// This code should be kept in sync on Fabric with analogous switch statement in
620626
// `- [RNSScreenStackHeaderConfig replaceNavigationBarViewsWithSnapshotOfSubview:]` method.
@@ -647,17 +653,16 @@ + (void)updateViewController:(UIViewController *)vc
647653

648654
if ([subview.subviews[0] isKindOfClass:[RNSSearchBar class]]) {
649655
#if !TARGET_OS_TV
650-
if (@available(iOS 11.0, *)) {
651-
RNSSearchBar *searchBar = subview.subviews[0];
652-
navitem.searchController = searchBar.controller;
653-
navitem.hidesSearchBarWhenScrolling = searchBar.hideWhenScrolling;
656+
RNSSearchBar *searchBar = subview.subviews[0];
657+
searchBarPresent = true;
658+
navitem.searchController = searchBar.controller;
659+
navitem.hidesSearchBarWhenScrolling = searchBar.hideWhenScrolling;
654660
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_16_0) && \
655661
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_16_0
656-
if (@available(iOS 16.0, *)) {
657-
navitem.preferredSearchBarPlacement = [searchBar placementAsUINavigationItemSearchBarPlacement];
658-
}
659-
#endif /* Check for iOS 16.0 */
662+
if (@available(iOS 16.0, *)) {
663+
navitem.preferredSearchBarPlacement = [searchBar placementAsUINavigationItemSearchBarPlacement];
660664
}
665+
#endif /* Check for iOS 16.0 */
661666
#endif /* !TARGET_OS_TV */
662667
}
663668
break;
@@ -668,6 +673,12 @@ + (void)updateViewController:(UIViewController *)vc
668673
}
669674
}
670675

676+
#if !TARGET_OS_TV
677+
if (!searchBarPresent) {
678+
navitem.searchController = nil;
679+
}
680+
#endif /* !TARGET_OS_TV */
681+
671682
// This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug).
672683
// See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments)
673684
navitem.title = config.title;

0 commit comments

Comments
 (0)