diff --git a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt index 10a36d1669..02fe8dc2d5 100644 --- a/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/ScreenStackHeaderConfigViewManager.kt @@ -201,4 +201,8 @@ class ScreenStackHeaderConfigViewManager : ViewGroupManager { void setTitleFontWeight(T view, @Nullable String value); void setTitleColor(T view, @Nullable Integer value); void setDisableBackButtonMenu(T view, boolean value); + void setBackButtonDisplayMode(T view, @Nullable String value); void setHideBackButton(T view, boolean value); void setBackButtonInCustomView(T view, boolean value); void setTopInsetEnabled(T view, boolean value); diff --git a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md index 4da3bb64a9..4437a1343a 100644 --- a/guides/GUIDE_FOR_LIBRARY_AUTHORS.md +++ b/guides/GUIDE_FOR_LIBRARY_AUTHORS.md @@ -445,6 +445,13 @@ Controls whether the stack should be in `rtl` or `ltr` form. Boolean indicating whether to show the menu on longPress of iOS >= 14 back button. +### `backButtonDisplayMode` (iOS only) + +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: +- `default` – show given back button previous controller title, system generic or just icon based on available space +- `generic` – show given system generic or just icon based on available space +- `minimal` – show just an icon + ### `hidden` 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`. diff --git a/ios/RNSConvert.h b/ios/RNSConvert.h index dcf0232636..7f816553c3 100644 --- a/ios/RNSConvert.h +++ b/ios/RNSConvert.h @@ -6,6 +6,12 @@ namespace react = facebook::react; @interface RNSConvert : NSObject ++ (UISemanticContentAttribute)UISemanticContentAttributeFromCppEquivalent: + (react::RNSScreenStackHeaderConfigDirection)direction; + ++ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent: + (react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode; + + (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent: (react::RNSScreenStackPresentation)stackPresentation; diff --git a/ios/RNSConvert.mm b/ios/RNSConvert.mm index 650da91631..2acf65554b 100644 --- a/ios/RNSConvert.mm +++ b/ios/RNSConvert.mm @@ -3,6 +3,30 @@ #ifdef RCT_NEW_ARCH_ENABLED @implementation RNSConvert ++ (UISemanticContentAttribute)UISemanticContentAttributeFromCppEquivalent: + (react::RNSScreenStackHeaderConfigDirection)direction +{ + switch (direction) { + case react::RNSScreenStackHeaderConfigDirection::Rtl: + return UISemanticContentAttributeForceRightToLeft; + case react::RNSScreenStackHeaderConfigDirection::Ltr: + return UISemanticContentAttributeForceLeftToRight; + } +} + ++ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayModeFromCppEquivalent: + (react::RNSScreenStackHeaderConfigBackButtonDisplayMode)backButtonDisplayMode +{ + switch (backButtonDisplayMode) { + case react::RNSScreenStackHeaderConfigBackButtonDisplayMode::Default: + return UINavigationItemBackButtonDisplayModeDefault; + case react::RNSScreenStackHeaderConfigBackButtonDisplayMode::Generic: + return UINavigationItemBackButtonDisplayModeGeneric; + case react::RNSScreenStackHeaderConfigBackButtonDisplayMode::Minimal: + return UINavigationItemBackButtonDisplayModeMinimal; + } +} + + (RNSScreenStackPresentation)RNSScreenStackPresentationFromCppEquivalent: (react::RNSScreenStackPresentation)stackPresentation { diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h index 83d4b61afe..d88f8f942e 100644 --- a/ios/RNSScreenStackHeaderConfig.h +++ b/ios/RNSScreenStackHeaderConfig.h @@ -55,6 +55,7 @@ @property (nonatomic) BOOL translucent; @property (nonatomic) BOOL backButtonInCustomView; @property (nonatomic) UISemanticContentAttribute direction; +@property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode; + (void)willShowViewController:(UIViewController *)vc animated:(BOOL)animated @@ -70,5 +71,6 @@ + (UIBlurEffectStyle)UIBlurEffectStyle:(id)json; + (UISemanticContentAttribute)UISemanticContentAttribute:(id)json; ++ (UINavigationItemBackButtonDisplayMode)UINavigationItemBackButtonDisplayMode:(id)json; @end diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 6596a8beef..a5a84a1f7f 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -19,6 +19,7 @@ #import #import #import +#import "RNSConvert.h" #import "RNSScreen.h" #import "RNSScreenStackHeaderConfig.h" #import "RNSSearchBar.h" @@ -513,6 +514,13 @@ + (void)updateViewController:(UIViewController *)vc auto isBackButtonCustomized = !isBackTitleBlank || config.disableBackButtonMenu; +#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_14_0) && \ + __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 + if (@available(iOS 14.0, *)) { + prevItem.backButtonDisplayMode = config.backButtonDisplayMode; + } +#endif + if (config.isBackTitleVisible) { if ((config.backTitleFontFamily && // While being used by react-navigation, the `backTitleFontFamily` will @@ -786,11 +794,6 @@ - (void)prepareForRecycle _initialPropsSet = NO; } -+ (react::ComponentDescriptorProvider)componentDescriptorProvider -{ - return react::concreteComponentDescriptorProvider(); -} - - (NSNumber *)getFontSizePropValue:(int)value { if (value > 0) @@ -798,14 +801,9 @@ - (NSNumber *)getFontSizePropValue:(int)value return nil; } -- (UISemanticContentAttribute)getDirectionPropValue:(react::RNSScreenStackHeaderConfigDirection)direction ++ (react::ComponentDescriptorProvider)componentDescriptorProvider { - switch (direction) { - case react::RNSScreenStackHeaderConfigDirection::Rtl: - return UISemanticContentAttributeForceRightToLeft; - case react::RNSScreenStackHeaderConfigDirection::Ltr: - return UISemanticContentAttributeForceLeftToRight; - } + return react::concreteComponentDescriptorProvider(); } - (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:: _backTitleFontSize = [self getFontSizePropValue:newScreenProps.backTitleFontSize]; _hideBackButton = newScreenProps.hideBackButton; _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; + _backButtonDisplayMode = + [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; if (newScreenProps.direction != oldScreenProps.direction) { - _direction = [self getDirectionPropValue:newScreenProps.direction]; + _direction = [RNSConvert UISemanticContentAttributeFromCppEquivalent:newScreenProps.direction]; } _backTitleVisible = newScreenProps.backTitleVisible; @@ -945,8 +945,8 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(hideShadow, BOOL) RCT_EXPORT_VIEW_PROPERTY(backButtonInCustomView, BOOL) RCT_EXPORT_VIEW_PROPERTY(disableBackButtonMenu, BOOL) -// `hidden` is an UIView property, we need to use different name internally -RCT_REMAP_VIEW_PROPERTY(hidden, hide, BOOL) +RCT_EXPORT_VIEW_PROPERTY(backButtonDisplayMode, UINavigationItemBackButtonDisplayMode) +RCT_REMAP_VIEW_PROPERTY(hidden, hide, BOOL) // `hidden` is an UIView property, we need to use different name internally RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL) @end @@ -1002,6 +1002,16 @@ + (NSMutableDictionary *)blurEffectsForIOSVersion UISemanticContentAttributeUnspecified, integerValue) +RCT_ENUM_CONVERTER( + UINavigationItemBackButtonDisplayMode, + (@{ + @"default" : @(UINavigationItemBackButtonDisplayModeDefault), + @"generic" : @(UINavigationItemBackButtonDisplayModeGeneric), + @"minimal" : @(UINavigationItemBackButtonDisplayModeMinimal), + }), + UINavigationItemBackButtonDisplayModeDefault, + integerValue) + RCT_ENUM_CONVERTER(UIBlurEffectStyle, ([self blurEffectsForIOSVersion]), UIBlurEffectStyleExtraLight, integerValue) @end diff --git a/native-stack/README.md b/native-stack/README.md index 3ce4b12a8d..f38cecb18a 100644 --- a/native-stack/README.md +++ b/native-stack/README.md @@ -79,6 +79,13 @@ String that applies `rtl` or `ltr` form to the stack. On Android, you have to ad Boolean indicating whether to show the menu on longPress of iOS >= 14 back button. +#### `backButtonDisplayMode` (iOS only) + +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: +- `default` – show given back button previous controller title, system generic or just icon based on available space +- `generic` – show given system generic or just icon based on available space +- `minimal` – show just an icon + #### `fullScreenSwipeEnabled` (iOS only) 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`. diff --git a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts index 9cbc3dad9e..0af3c1ce28 100644 --- a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts +++ b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts @@ -13,6 +13,8 @@ type OnAttachedEvent = Readonly<{}>; // eslint-disable-next-line @typescript-eslint/ban-types type OnDetachedEvent = Readonly<{}>; +type BackButtonDisplayMode = 'minimal' | 'default' | 'generic'; + export interface NativeProps extends ViewProps { onAttached?: DirectEventHandler; onDetached?: DirectEventHandler; @@ -39,6 +41,7 @@ export interface NativeProps extends ViewProps { titleFontWeight?: string; titleColor?: ColorValue; disableBackButtonMenu?: boolean; + backButtonDisplayMode?: WithDefault; hideBackButton?: boolean; backButtonInCustomView?: boolean; // TODO: implement this props on iOS diff --git a/src/native-stack/types.tsx b/src/native-stack/types.tsx index a79ba4c685..f461ed2972 100644 --- a/src/native-stack/types.tsx +++ b/src/native-stack/types.tsx @@ -112,6 +112,15 @@ export type NativeStackNavigationOptions = { * @platform ios */ disableBackButtonMenu?: boolean; + /** + * How the back button behaves by default (when not customized). Available on iOS>=14, and is used only when none of: `backTitleFontFamily`, `backTitleFontSize`, `disableBackButtonMenu` or `backTitle` is set. + * The following values are currently supported (they correspond to https://developer.apple.com/documentation/uikit/uinavigationitembackbuttondisplaymode?language=objc): + * - "default" – show given back button previous controller title, system generic or just icon based on available space + * - "generic" – show given system generic or just icon based on available space + * - "minimal" – show just an icon + * @platform ios + */ + backButtonDisplayMode?: ScreenStackHeaderConfigProps['backButtonDisplayMode']; /** * Whether inactive screens should be suspended from re-rendering. Defaults to `false`. * Defaults to `true` when `enableFreeze()` is run at the top of the application. diff --git a/src/native-stack/views/HeaderConfig.tsx b/src/native-stack/views/HeaderConfig.tsx index f1ed55d820..0be5f1d58b 100644 --- a/src/native-stack/views/HeaderConfig.tsx +++ b/src/native-stack/views/HeaderConfig.tsx @@ -27,6 +27,7 @@ export default function HeaderConfig({ backButtonInCustomView, direction, disableBackButtonMenu, + backButtonDisplayMode = 'default', headerBackTitle, headerBackTitleStyle = {}, headerBackTitleVisible = true, @@ -120,6 +121,7 @@ export default function HeaderConfig({ color={tintColor} direction={direction} disableBackButtonMenu={disableBackButtonMenu} + backButtonDisplayMode={backButtonDisplayMode} hidden={headerShown === false} hideBackButton={headerHideBackButton} hideShadow={headerHideShadow} diff --git a/src/types.tsx b/src/types.tsx index c29f9d90f8..b67831f696 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -19,6 +19,7 @@ export type SearchBarCommands = { cancelSearch: () => void; }; +export type BackButtonDisplayMode = 'default' | 'generic' | 'minimal'; export type StackPresentationTypes = | 'push' | 'modal' @@ -461,6 +462,15 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { * @platform ios */ disableBackButtonMenu?: boolean; + /** + * How the back button behaves by default (when not customized). Available on iOS>=14, and is used only when none of: `backTitleFontFamily`, `backTitleFontSize`, `disableBackButtonMenu` or `backTitle` is set. + * The following values are currently supported (they correspond to https://developer.apple.com/documentation/uikit/uinavigationitembackbuttondisplaymode?language=objc): + * - "default" – show given back button previous controller title, system generic or just icon based on available space + * - "generic" – show given system generic or just icon based on available space + * - "minimal" – show just an icon + * @platform ios + */ + backButtonDisplayMode?: BackButtonDisplayMode; /** * 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. */