Skip to content

Commit b47c4ac

Browse files
maciekstosiokkafar
andauthored
feat(iOS): Add support for UINavigationBackButtonDisplayMode (#2123)
## Description ~This PR improves upon #2105. #2105 allowed to use iOS 14 default back button behavior when label is not provided. This PR allows to modify the behavior by allowing to provide UINavigationButtonBackButtonDisplayMode and enables it for custom text (without style modifications). The main problem is that we used to provide backButtonItem in most of the cases which [disables](https://developer.apple.com/documentation/uikit/uinavigationitem/3656350-backbuttondisplaymode) backButtonDisplayMode.~ This PR adds possibility to customize default behavior of back button using `backButtonDisplayMode` ([UINavigationBackButtonDisplayMode](https://developer.apple.com/documentation/uikit/uinavigationitem/backbuttondisplaymode)) for iOS. :warning: **This modifies only default back button**, when any customization is added (including headerBackTitle) in native part we create custom `RNSUIBarButtonItem` and set it as `backButtonItem`, which [disables](https://developer.apple.com/documentation/uikit/uinavigationitem/3656350-backbuttondisplaymode) `backButtonDisplayMode` behavior. I tried to make it work together with custom label (`headerBackTitle`) using `prevItem.backButtonTitle`, but due to iOS limitations it is not viable option. It influences also back button menu - changes the label of previous screen - which is not the behavior we want. To sum up, `backButtonDisplayMode` work when none of: - `headerBackTitleStyle.fontFamily` - `headerBackTitleStyle.fontSize` - `headerBackTitle` - `disableBackButtonMenu` are set. ## Screenshots / GIFs |Paper|Fabric| |-|-| |<video src="https://github.com/software-mansion/react-native-screens/assets/11800297/c6aa7697-4331-4cb4-a81d-7f77f128513d" />|<video src="https://github.com/software-mansion/react-native-screens/assets/11800297/fa0edd92-1aa2-45e5-a466-516c0ec120d2" />| <details> <summary>Example component used in tests:</summary> ```jsx import * as React from 'react'; import { Button, View, Text, StyleSheet } from 'react-native'; import { NavigationContainer, ParamListBase } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; const Stack = createNativeStackNavigator(); type NavProp = { navigation: NativeStackNavigationProp<ParamListBase>; }; export default function App() { return ( <NavigationContainer> <Stack.Navigator> <Stack.Screen name="screenA" component={ScreenA} options={{ headerTitle: 'A: Home' }} /> <Stack.Screen name="screenB" component={ScreenB} options={{ headerTitle: 'B: default', backButtonDisplayMode: 'default', }} /> <Stack.Screen name="screenC" component={ScreenC} options={{ headerTitle: 'C: generic', backButtonDisplayMode: 'generic', }} /> <Stack.Screen name="screenD" component={ScreenD} options={{ headerTitle: 'D: minimal', backButtonDisplayMode: 'minimal', }} /> <Stack.Screen name="screenE" component={ScreenE} options={{ headerTitle: 'E: custom', headerBackTitle: 'Back Title', backButtonDisplayMode: 'minimal', }} /> </Stack.Navigator> </NavigationContainer> ); } const ScreenA = ({ navigation }: NavProp) => ( <View style={styles.container}> <Text>Screen A</Text> <Button onPress={() => navigation.navigate('screenB')} title="Go to screen B" /> </View> ); const ScreenB = ({ navigation }: NavProp) => ( <View style={styles.container}> <Text>Screen B</Text> <Text>backButtonDisplayMode: default</Text> <Button onPress={() => navigation.navigate('screenC')} title="Go to screen C" /> </View> ); const ScreenC = ({ navigation }: NavProp) => ( <View style={{ flex: 1, paddingTop: 50 }}> <Text>Screen C</Text> <Text>backButtonDisplayMode: generic</Text> <Button onPress={() => navigation.navigate('screenD')} title="Go to screen D" /> </View> ); const ScreenD = ({ navigation }: NavProp) => ( <View style={styles.container}> <Text>Screen D</Text> <Text>backButtonDisplayMode: minimal</Text> <Button onPress={() => navigation.navigate('screenE')} title="Go to screen E" /> </View> ); const ScreenE = (_props: NavProp) => ( <View style={styles.container}> <Text>Screen E</Text> <Text>backButtonDisplayMode omitted because of the headerBackTitle</Text> </View> ); const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'space-around' }, }); ``` </details> ## Checklist - [x] Included code example that can be used to test this change - [x] Updated TS types - [x] Updated documentation: <!-- For adding new props to native-stack --> - [x] https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md - [x] https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md - [x] https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx - [x] https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx - [x] Ensured that CI passes Tested #1864: Paper ✅ Fabric ✅ Tested #1646: Paper ❌ Fabric ❌ - but it does not work on main too, could now be achieved using `backButtonDisplayMode: ‘minimal’` --------- Co-authored-by: Kacper Kafara <[email protected]>
1 parent f3630d9 commit b47c4ac

File tree

13 files changed

+103
-15
lines changed

13 files changed

+103
-15
lines changed

android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,8 @@ class ScreenStackHeaderConfigViewManager : ViewGroupManager<ScreenStackHeaderCon
201201
override fun setDisableBackButtonMenu(view: ScreenStackHeaderConfig?, value: Boolean) {
202202
logNotAvailable("disableBackButtonMenu")
203203
}
204+
205+
override fun setBackButtonDisplayMode(view: ScreenStackHeaderConfig?, value: String?) {
206+
logNotAvailable("backButtonDisplayMode")
207+
}
204208
}

android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerDelegate.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
9191
case "disableBackButtonMenu":
9292
mViewManager.setDisableBackButtonMenu(view, value == null ? false : (boolean) value);
9393
break;
94+
case "backButtonDisplayMode":
95+
mViewManager.setBackButtonDisplayMode(view, (String) value);
96+
break;
9497
case "hideBackButton":
9598
mViewManager.setHideBackButton(view, value == null ? false : (boolean) value);
9699
break;

android/src/paper/java/com/facebook/react/viewmanagers/RNSScreenStackHeaderConfigManagerInterface.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public interface RNSScreenStackHeaderConfigManagerInterface<T extends View> {
3636
void setTitleFontWeight(T view, @Nullable String value);
3737
void setTitleColor(T view, @Nullable Integer value);
3838
void setDisableBackButtonMenu(T view, boolean value);
39+
void setBackButtonDisplayMode(T view, @Nullable String value);
3940
void setHideBackButton(T view, boolean value);
4041
void setBackButtonInCustomView(T view, boolean value);
4142
void setTopInsetEnabled(T view, boolean value);

guides/GUIDE_FOR_LIBRARY_AUTHORS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,13 @@ Controls whether the stack should be in `rtl` or `ltr` form.
445445

446446
Boolean indicating whether to show the menu on longPress of iOS >= 14 back button.
447447

448+
### `backButtonDisplayMode` (iOS only)
449+
450+
Enum value indicating display mode of **default** back button. It works on iOS >= 14, and is used only when none of: `backTitleFontFamily`, `backTitleFontSize`, `disableBackButtonMenu` or `backTitle` is set. Otherwise, when the button is customized, under the hood we use iOS native `backButtonItem` which overrides `backButtonDisplayMode`. Read more [#2123](https://github.com/software-mansion/react-native-screens/pull/2123). Possible options:
451+
- `default` – show given back button previous controller title, system generic or just icon based on available space
452+
- `generic` – show given system generic or just icon based on available space
453+
- `minimal` – show just an icon
454+
448455
### `hidden`
449456

450457
When set to `true` the header will be hidden while the parent `Screen` is on the top of the stack. The default value is `false`.

ios/RNSConvert.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@ namespace react = facebook::react;
66

77
@interface RNSConvert : NSObject
88

9+
+ (UISemanticContentAttribute)UISemanticContentAttributeFromCppEquivalent:
10+
(react::RNSScreenStackHeaderConfigDirection)direction;
11+
12+
+ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent:
13+
(react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode;
14+
915
+ (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
1016
(react::RNSScreenStackPresentation)stackPresentation;
1117

ios/RNSConvert.mm

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,30 @@
33
#ifdef RCT_NEW_ARCH_ENABLED
44
@implementation RNSConvert
55

6+
+ (UISemanticContentAttribute)UISemanticContentAttributeFromCppEquivalent:
7+
(react::RNSScreenStackHeaderConfigDirection)direction
8+
{
9+
switch (direction) {
10+
case react::RNSScreenStackHeaderConfigDirection::Rtl:
11+
return UISemanticContentAttributeForceRightToLeft;
12+
case react::RNSScreenStackHeaderConfigDirection::Ltr:
13+
return UISemanticContentAttributeForceLeftToRight;
14+
}
15+
}
16+
17+
+ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent:
18+
(react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode
19+
{
20+
switch (backButtonDisplayMode) {
21+
case react::RNSScreenStackHeaderConfigBackButtonDisplayMode::Default:
22+
return UINavigationItemBackButtonDisplayModeDefault;
23+
case react::RNSScreenStackHeaderConfigBackButtonDisplayMode::Generic:
24+
return UINavigationItemBackButtonDisplayModeGeneric;
25+
case react::RNSScreenStackHeaderConfigBackButtonDisplayMode::Minimal:
26+
return UINavigationItemBackButtonDisplayModeMinimal;
27+
}
28+
}
29+
630
+ (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent:
731
(react::RNSScreenStackPresentation)stackPresentation
832
{

ios/RNSScreenStackHeaderConfig.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
@property (nonatomic) BOOL translucent;
5656
@property (nonatomic) BOOL backButtonInCustomView;
5757
@property (nonatomic) UISemanticContentAttribute direction;
58+
@property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode;
5859

5960
+ (void)willShowViewController:(UIViewController *)vc
6061
animated:(BOOL)animated
@@ -70,5 +71,6 @@
7071

7172
+ (UIBlurEffectStyle)UIBlurEffectStyle:(id)json;
7273
+ (UISemanticContentAttribute)UISemanticContentAttribute:(id)json;
74+
+ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayMode:(id)json;
7375

7476
@end

ios/RNSScreenStackHeaderConfig.mm

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#import <React/RCTFont.h>
2020
#import <React/RCTImageLoader.h>
2121
#import <React/RCTImageSource.h>
22+
#import "RNSConvert.h"
2223
#import "RNSScreen.h"
2324
#import "RNSScreenStackHeaderConfig.h"
2425
#import "RNSSearchBar.h"
@@ -513,6 +514,13 @@ + (void)updateViewController:(UIViewController *)vc
513514

514515
auto isBackButtonCustomized = !isBackTitleBlank || config.disableBackButtonMenu;
515516

517+
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_14_0) && \
518+
__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0
519+
if (@available(iOS 14.0, *)) {
520+
prevItem.backButtonDisplayMode = config.backButtonDisplayMode;
521+
}
522+
#endif
523+
516524
if (config.isBackTitleVisible) {
517525
if ((config.backTitleFontFamily &&
518526
// While being used by react-navigation, the `backTitleFontFamily` will
@@ -786,26 +794,16 @@ - (void)prepareForRecycle
786794
_initialPropsSet = NO;
787795
}
788796

789-
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
790-
{
791-
return react::concreteComponentDescriptorProvider<react::RNSScreenStackHeaderConfigComponentDescriptor>();
792-
}
793-
794797
- (NSNumber *)getFontSizePropValue:(int)value
795798
{
796799
if (value > 0)
797800
return [NSNumber numberWithInt:value];
798801
return nil;
799802
}
800803

801-
- (UISemanticContentAttribute)getDirectionPropValue:(react::RNSScreenStackHeaderConfigDirection)direction
804+
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
802805
{
803-
switch (direction) {
804-
case react::RNSScreenStackHeaderConfigDirection::Rtl:
805-
return UISemanticContentAttributeForceRightToLeft;
806-
case react::RNSScreenStackHeaderConfigDirection::Ltr:
807-
return UISemanticContentAttributeForceLeftToRight;
808-
}
806+
return react::concreteComponentDescriptorProvider<react::RNSScreenStackHeaderConfigComponentDescriptor>();
809807
}
810808

811809
- (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::Shared const &)oldProps
@@ -852,9 +850,11 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::
852850
_backTitleFontSize = [self getFontSizePropValue:newScreenProps.backTitleFontSize];
853851
_hideBackButton = newScreenProps.hideBackButton;
854852
_disableBackButtonMenu = newScreenProps.disableBackButtonMenu;
853+
_backButtonDisplayMode =
854+
[RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode];
855855

856856
if (newScreenProps.direction != oldScreenProps.direction) {
857-
_direction = [self getDirectionPropValue:newScreenProps.direction];
857+
_direction = [RNSConvert UISemanticContentAttributeFromCppEquivalent:newScreenProps.direction];
858858
}
859859

860860
_backTitleVisible = newScreenProps.backTitleVisible;
@@ -945,8 +945,8 @@ - (UIView *)view
945945
RCT_EXPORT_VIEW_PROPERTY(hideShadow, BOOL)
946946
RCT_EXPORT_VIEW_PROPERTY(backButtonInCustomView, BOOL)
947947
RCT_EXPORT_VIEW_PROPERTY(disableBackButtonMenu, BOOL)
948-
// `hidden` is an UIView property, we need to use different name internally
949-
RCT_REMAP_VIEW_PROPERTY(hidden, hide, BOOL)
948+
RCT_EXPORT_VIEW_PROPERTY(backButtonDisplayMode, UINavigationItemBackButtonDisplayMode)
949+
RCT_REMAP_VIEW_PROPERTY(hidden, hide, BOOL) // `hidden` is an UIView property, we need to use different name internally
950950
RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL)
951951

952952
@end
@@ -1002,6 +1002,16 @@ + (NSMutableDictionary *)blurEffectsForIOSVersion
10021002
UISemanticContentAttributeUnspecified,
10031003
integerValue)
10041004

1005+
RCT_ENUM_CONVERTER(
1006+
UINavigationItemBackButtonDisplayMode,
1007+
(@{
1008+
@"default" : @(UINavigationItemBackButtonDisplayModeDefault),
1009+
@"generic" : @(UINavigationItemBackButtonDisplayModeGeneric),
1010+
@"minimal" : @(UINavigationItemBackButtonDisplayModeMinimal),
1011+
}),
1012+
UINavigationItemBackButtonDisplayModeDefault,
1013+
integerValue)
1014+
10051015
RCT_ENUM_CONVERTER(UIBlurEffectStyle, ([self blurEffectsForIOSVersion]), UIBlurEffectStyleExtraLight, integerValue)
10061016

10071017
@end

native-stack/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ String that applies `rtl` or `ltr` form to the stack. On Android, you have to ad
7979

8080
Boolean indicating whether to show the menu on longPress of iOS >= 14 back button.
8181

82+
#### `backButtonDisplayMode` (iOS only)
83+
84+
Enum value indicating display mode of **default** back button. It works on iOS >= 14, and is used only when none of: `backTitleFontFamily`, `backTitleFontSize`, `disableBackButtonMenu` or `backTitle` is set. Otherwise, when the button is customized, under the hood we use iOS native `backButtonItem` which overrides `backButtonDisplayMode`. Read more [#2123](https://github.com/software-mansion/react-native-screens/pull/2123). Possible options:
85+
- `default` – show given back button previous controller title, system generic or just icon based on available space
86+
- `generic` – show given system generic or just icon based on available space
87+
- `minimal` – show just an icon
88+
8289
#### `fullScreenSwipeEnabled` (iOS only)
8390

8491
Boolean indicating whether the swipe gesture should work on whole screen. Swiping with this option results in the same transition animation as `simple_push` by default. It can be changed to other custom animations with `customAnimationOnSwipe` prop, but default iOS swipe animation is not achievable due to usage of custom recognizer. Defaults to `false`.

src/fabric/ScreenStackHeaderConfigNativeComponent.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ type OnAttachedEvent = Readonly<{}>;
1313
// eslint-disable-next-line @typescript-eslint/ban-types
1414
type OnDetachedEvent = Readonly<{}>;
1515

16+
type BackButtonDisplayMode = 'minimal' | 'default' | 'generic';
17+
1618
export interface NativeProps extends ViewProps {
1719
onAttached?: DirectEventHandler<OnAttachedEvent>;
1820
onDetached?: DirectEventHandler<OnDetachedEvent>;
@@ -39,6 +41,7 @@ export interface NativeProps extends ViewProps {
3941
titleFontWeight?: string;
4042
titleColor?: ColorValue;
4143
disableBackButtonMenu?: boolean;
44+
backButtonDisplayMode?: WithDefault<BackButtonDisplayMode, 'default'>;
4245
hideBackButton?: boolean;
4346
backButtonInCustomView?: boolean;
4447
// TODO: implement this props on iOS

0 commit comments

Comments
 (0)