From bec9396f402c9dab121315c0166367d16281f1d7 Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Thu, 16 Oct 2025 12:06:59 +0200 Subject: [PATCH 1/8] introduce new logic for creating back button --- ios/RNSScreenStackHeaderConfig.mm | 184 +++++++++++++++++++++--------- 1 file changed, 132 insertions(+), 52 deletions(-) diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 578bd88c7d..82f0727832 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -755,67 +755,147 @@ - (void)configureBackItem:(nullable UINavigationItem *)prevItem const auto *config = self; - const auto isBackTitleBlank = [NSString rnscreens_isBlankOrNull:config.backTitle] == YES; - NSString *resolvedBackTitle = isBackTitleBlank ? prevItem.title : config.backTitle; - // If previous screen controller was recreated (e.g. when you go back to tab with stack that has multiple screens), // its navigationItem may not have any information from screen's headerConfig, including the title. - // If this is the case, we attempt to extract the title from previous screen's config directly. - if (resolvedBackTitle == nil && [prevVC isKindOfClass:[RNSScreen class]]) { + // If this is the case, we attempt to set the title using previous screen's config directly. + if (prevItem.title == nil && [prevVC isKindOfClass:[RNSScreen class]]) { RNSScreen *prevScreen = static_cast(prevVC); - resolvedBackTitle = prevScreen.screenView.findHeaderConfig.title; + RNSScreenStackHeaderConfig *prevConfig = prevScreen.screenView.findHeaderConfig; + if (prevConfig != nil) { + prevItem.title = prevConfig.title; + } } - prevItem.backButtonTitle = resolvedBackTitle; - // This has any effect only in case the `backBarButtonItem` is not set. - // We apply it before we configure the back item, because it might get overriden. - prevItem.backButtonDisplayMode = config.backButtonDisplayMode; + BOOL usesCustomFont = + (config.backTitleFontFamily && + // While being used by react-navigation, the `backTitleFontFamily` will + // be set to "System" by default - which is the system default font. + // To avoid always considering the font as customized, we need to have an additional check. + // See: https://github.com/software-mansion/react-native-screens/pull/2105#discussion_r1565222738 + ![config.backTitleFontFamily isEqual:@"System"]) || + config.backTitleFontSize; + + BOOL shouldUseCustomBackBarButtonItem = usesCustomFont || config.disableBackButtonMenu; + if (shouldUseCustomBackBarButtonItem) { + [self configureCustomBackItem:prevItem usingCustomFont:usesCustomFont]; + } else { + [self configureNativeBackItem:prevItem]; + } +#endif +} - if (config.isBackTitleVisible) { - RNSBackBarButtonItem *backBarButtonItem = [[RNSBackBarButtonItem alloc] initWithTitle:resolvedBackTitle - style:UIBarButtonItemStylePlain - target:nil - action:nil]; - auto shouldUseCustomBackBarButtonItem = config.disableBackButtonMenu; - [backBarButtonItem setMenuHidden:config.disableBackButtonMenu]; - - if ((config.backTitleFontFamily && - // While being used by react-navigation, the `backTitleFontFamily` will - // be set to "System" by default - which is the system default font. - // To avoid always considering the font as customized, we need to have an additional check. - // See: https://github.com/software-mansion/react-native-screens/pull/2105#discussion_r1565222738 - ![config.backTitleFontFamily isEqual:@"System"]) || - config.backTitleFontSize) { - shouldUseCustomBackBarButtonItem = YES; - NSMutableDictionary *attrs = [NSMutableDictionary new]; - NSNumber *size = config.backTitleFontSize ?: @17; - if (config.backTitleFontFamily) { - attrs[NSFontAttributeName] = [RCTFont updateFont:nil - withFamily:config.backTitleFontFamily - size:size - weight:nil - style:nil - variant:nil - scaleMultiplier:1.0]; - } else { - attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:[size floatValue]]; - } - [RNSScreenStackHeaderConfig setTitleAttibutes:attrs forButton:backBarButtonItem]; - } +- (void)configureCustomBackItem:(nonnull UINavigationItem *)prevItem + usingCustomFont:(BOOL)usesCustomFont API_UNAVAILABLE(tvos) +{ + const auto *config = self; - // Prevent unnecessary assignment of backBarButtonItem if it is not customized, - // as assigning one will override the native behavior of automatically shortening - // the title to "Back" or hide the back title if there's not enough space. - // See: https://github.com/software-mansion/react-native-screens/issues/1589 - if (shouldUseCustomBackBarButtonItem) { - prevItem.backBarButtonItem = backBarButtonItem; - } + NSString *resolvedBackTitle = nil; + if (@available(iOS 26, *)) { + resolvedBackTitle = [self resolveCustomBackItemTitleForSystemVersion26AndAboveWithPrevItem:prevItem]; } else { - // back button title should be not visible next to back button, - // but it should still appear in back menu - prevItem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeMinimal; + resolvedBackTitle = [self resolveCustomBackItemTitleForSystemVersionPriorTo26WithPrevItem:prevItem]; } -#endif + + RNSBackBarButtonItem *backBarButtonItem = [[RNSBackBarButtonItem alloc] initWithTitle:resolvedBackTitle + style:UIBarButtonItemStylePlain + target:nil + action:nil]; + [backBarButtonItem setMenuHidden:config.disableBackButtonMenu]; + + if (usesCustomFont) { + [self configureCustomFontForCustomBackButton:backBarButtonItem backItem:prevItem]; + } + + prevItem.backBarButtonItem = backBarButtonItem; +} + +- (NSString *)resolveCustomBackItemTitleForSystemVersionPriorTo26WithPrevItem:(nonnull UINavigationItem *)prevItem +{ + const auto *config = self; + + NSString *resolvedBackTitle = nil; + switch (config.backButtonDisplayMode) { + // We're unable to provide generic back button title for DisplayModeGeneric when using custom back item, + // that's why we fallback to DisplayModeDefault behavior. + case UINavigationItemBackButtonDisplayModeDefault: + case UINavigationItemBackButtonDisplayModeGeneric: + resolvedBackTitle = [NSString rnscreens_isBlankOrNull:config.backTitle] ? prevItem.title : config.backTitle; + break; + + case UINavigationItemBackButtonDisplayModeMinimal: + resolvedBackTitle = nil; + break; + + default: + RCTLogError(@"[RNScreens] Unsupported UINavigationItemBackButtonDisplayMode"); + break; + } + + return resolvedBackTitle; +} + +- (NSString *)resolveCustomBackItemTitleForSystemVersion26AndAboveWithPrevItem:(nonnull UINavigationItem *)prevItem +{ + const auto *config = self; + + NSString *resolvedBackTitle = nil; + switch (config.backButtonDisplayMode) { + case UINavigationItemBackButtonDisplayModeDefault: + resolvedBackTitle = [NSString rnscreens_isBlankOrNull:config.backTitle] ? nil : config.backTitle; + break; + + // Starting from iOS 26, DisplayModeGeneric and DisplayModeMinimal behave in the same way. + case UINavigationItemBackButtonDisplayModeGeneric: + case UINavigationItemBackButtonDisplayModeMinimal: + resolvedBackTitle = nil; + break; + + default: + RCTLogError(@"[RNScreens] Unsupported UINavigationItemBackButtonDisplayMode"); + break; + } + + return resolvedBackTitle; +} + +- (void)configureCustomFontForCustomBackButton:(nonnull RNSBackBarButtonItem *)backBarButtonItem + backItem:(nonnull UINavigationItem *)prevItem +{ + const auto *config = self; + + NSMutableDictionary *attrs = [NSMutableDictionary new]; + NSNumber *size = config.backTitleFontSize ?: @17; + if (config.backTitleFontFamily) { + attrs[NSFontAttributeName] = [RCTFont updateFont:nil + withFamily:config.backTitleFontFamily + size:size + weight:nil + style:nil + variant:nil + scaleMultiplier:1.0]; + } else { + attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:[size floatValue]]; + } + [RNSScreenStackHeaderConfig setTitleAttibutes:attrs forButton:backBarButtonItem]; +} + +- (void)configureNativeBackItem:(nonnull UINavigationItem *)prevItem API_UNAVAILABLE(tvos) +{ + const auto *config = self; + + prevItem.backButtonDisplayMode = config.backButtonDisplayMode; + + // We set `backButtonTitle` only if we want to use custom title + // as starting from iOS 26, the behavior differs between `title` and `backButtonTitle`: + // `title` is not shown in back button whereas `backButtonTitle` is. + if ([NSString rnscreens_isBlankOrNull:config.backTitle]) { + prevItem.backButtonTitle = nil; + } else { + prevItem.backButtonTitle = config.backTitle; + } + + // Make sure that we don't use custom back button + prevItem.backBarButtonItem = nil; } - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController *)navCtrl From b8b24948d7ecddf6d4a51daecd73d2c013f99ec7 Mon Sep 17 00:00:00 2001 From: Krzysztof Ligarski Date: Thu, 16 Oct 2025 12:10:14 +0200 Subject: [PATCH 2/8] add new cases to Test2809 --- apps/src/tests/Test2809/index.tsx | 358 ++++++++++++++++++++++++++---- 1 file changed, 309 insertions(+), 49 deletions(-) diff --git a/apps/src/tests/Test2809/index.tsx b/apps/src/tests/Test2809/index.tsx index e44c724fb2..3ef0212394 100644 --- a/apps/src/tests/Test2809/index.tsx +++ b/apps/src/tests/Test2809/index.tsx @@ -10,45 +10,190 @@ import { createStackWithOptions } from './Shared'; // Naming convention: // Enabled/Disabled - based on headerBackButtonMenu value // Default/Generic/Minimal - based on headerBackButtonDisplayMode value -// DefaultText/CustomText/StyledText - based on label content +// DefaultText/CustomText/StyledText/CustomStyledText - based on label content // headerBackButtonMenu: enabled // headerBackButtonDisplayMode: default const EnabledDefaultDefaultText = createStackWithOptions({}, {}); -const EnabledDefaultCustomText = createStackWithOptions({}, { headerBackTitle: 'Custom' }); -const EnabledDefaultStyledText = createStackWithOptions({}, { headerBackTitleStyle: {fontSize: 30} }); +const EnabledDefaultCustomText = createStackWithOptions( + {}, + { headerBackTitle: 'Custom' }, +); +const EnabledDefaultStyledText = createStackWithOptions( + {}, + { headerBackTitleStyle: { fontSize: 30 } }, +); +const EnabledDefaultCustomStyledText = createStackWithOptions( + {}, + { headerBackTitle: 'Custom', headerBackTitleStyle: { fontSize: 30 } }, +); // headerBackButtonDisplayMode: generic -const EnabledGenericDefaultText = createStackWithOptions({}, { headerBackButtonDisplayMode: 'generic' }); -const EnabledGenericCustomText = createStackWithOptions({}, { headerBackButtonDisplayMode: 'generic', headerBackTitle: 'Custom' }); -const EnabledGenericStyledText = createStackWithOptions({}, { headerBackButtonDisplayMode: 'generic', headerBackTitleStyle: {fontSize: 30} }); +const EnabledGenericDefaultText = createStackWithOptions( + {}, + { headerBackButtonDisplayMode: 'generic' }, +); +const EnabledGenericCustomText = createStackWithOptions( + {}, + { headerBackButtonDisplayMode: 'generic', headerBackTitle: 'Custom' }, +); +const EnabledGenericStyledText = createStackWithOptions( + {}, + { + headerBackButtonDisplayMode: 'generic', + headerBackTitleStyle: { fontSize: 30 }, + }, +); +const EnabledGenericCustomStyledText = createStackWithOptions( + {}, + { + headerBackButtonDisplayMode: 'generic', + headerBackTitle: 'Custom', + headerBackTitleStyle: { fontSize: 30 }, + }, +); // headerBackButtonDisplayMode: generic -const EnabledMinimalDefaultText = createStackWithOptions({}, { headerBackButtonDisplayMode: 'minimal' }); -const EnabledMinimalCustomText = createStackWithOptions({}, { headerBackButtonDisplayMode: 'minimal', headerBackTitle: 'Custom' }); -const EnabledMinimalStyledText = createStackWithOptions({}, { headerBackButtonDisplayMode: 'minimal', headerBackTitleStyle: {fontSize: 30} }); +const EnabledMinimalDefaultText = createStackWithOptions( + {}, + { headerBackButtonDisplayMode: 'minimal' }, +); +const EnabledMinimalCustomText = createStackWithOptions( + {}, + { headerBackButtonDisplayMode: 'minimal', headerBackTitle: 'Custom' }, +); +const EnabledMinimalStyledText = createStackWithOptions( + {}, + { + headerBackButtonDisplayMode: 'minimal', + headerBackTitleStyle: { fontSize: 30 }, + }, +); +const EnabledMinimalCustomStyledText = createStackWithOptions( + {}, + { + headerBackButtonDisplayMode: 'minimal', + headerBackTitle: 'Custom', + headerBackTitleStyle: { fontSize: 30 }, + }, +); // headerBackButtonMenu: disabled // headerBackButtonDisplayMode: default -const DisabledDefaultDefaultText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false }); -const DisabledDefaultCustomText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false, headerBackTitle: 'Custom' }); -const DisabledDefaultStyledText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false, headerBackTitleStyle: {fontSize: 30}}); +const DisabledDefaultDefaultText = createStackWithOptions( + {}, + { headerBackButtonMenuEnabled: false }, +); +const DisabledDefaultCustomText = createStackWithOptions( + {}, + { headerBackButtonMenuEnabled: false, headerBackTitle: 'Custom' }, +); +const DisabledDefaultStyledText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackTitleStyle: { fontSize: 30 }, + }, +); +const DisabledDefaultCustomStyledText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackTitle: 'Custom', + headerBackTitleStyle: { fontSize: 30 }, + }, +); // headerBackButtonDisplayMode: generic -const DisabledGenericDefaultText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false, headerBackButtonDisplayMode: 'generic' }); -const DisabledGenericCustomText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false, headerBackButtonDisplayMode: 'generic', headerBackTitle: 'Custom' }); -const DisabledGenericStyledText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false, headerBackButtonDisplayMode: 'generic', headerBackTitleStyle: {fontSize: 30} }); +const DisabledGenericDefaultText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackButtonDisplayMode: 'generic', + }, +); +const DisabledGenericCustomText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackButtonDisplayMode: 'generic', + headerBackTitle: 'Custom', + }, +); +const DisabledGenericStyledText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackButtonDisplayMode: 'generic', + headerBackTitleStyle: { fontSize: 30 }, + }, +); +const DisabledGenericCustomStyledText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackTitle: 'Custom', + headerBackButtonDisplayMode: 'generic', + headerBackTitleStyle: { fontSize: 30 }, + }, +); // headerBackButtonDisplayMode: generic -const DisabledMinimalDefaultText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false, headerBackButtonDisplayMode: 'minimal' }); -const DisabledMinimalCustomText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false, headerBackButtonDisplayMode: 'minimal', headerBackTitle: 'Custom' }); -const DisabledMinimalStyledText = createStackWithOptions({}, { headerBackButtonMenuEnabled: false, headerBackButtonDisplayMode: 'minimal', headerBackTitleStyle: {fontSize: 30} }); +const DisabledMinimalDefaultText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackButtonDisplayMode: 'minimal', + }, +); +const DisabledMinimalCustomText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackButtonDisplayMode: 'minimal', + headerBackTitle: 'Custom', + }, +); +const DisabledMinimalStyledText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackButtonDisplayMode: 'minimal', + headerBackTitleStyle: { fontSize: 30 }, + }, +); +const DisabledMinimalCustomStyledText = createStackWithOptions( + {}, + { + headerBackButtonMenuEnabled: false, + headerBackTitle: 'Custom', + headerBackButtonDisplayMode: 'minimal', + headerBackTitleStyle: { fontSize: 30 }, + }, +); // Custom -const CustomLongDefaultText = createStackWithOptions({ headerTitle: 'LongLongLongLongLong'}, {}); -const CustomDefaultTextWithLongTitle = createStackWithOptions({}, {headerTitle: 'LongLongLongLongLongLongLong'}); -const CustomLongCustomText = createStackWithOptions({}, { headerBackTitle: 'LongLongLongLongLong' }); -const CustomCustomTextWithLongTitle = createStackWithOptions({}, {headerBackTitle: 'CustomBack', headerTitle: 'LongLongLongLongLongLongLong'}); - -const Button = ({title, onPress}: {title: string, onPress: () => void}) => ( - - {title} +const CustomLongDefaultText = createStackWithOptions( + { headerTitle: 'LongLongLongLongLong' }, + {}, +); +const CustomDefaultTextWithLongTitle = createStackWithOptions( + {}, + { headerTitle: 'LongLongLongLongLongLongLong' }, +); +const CustomLongCustomText = createStackWithOptions( + {}, + { headerBackTitle: 'LongLongLongLongLong' }, +); +const CustomCustomTextWithLongTitle = createStackWithOptions( + {}, + { + headerBackTitle: 'CustomBack', + headerTitle: 'LongLongLongLongLongLongLong', + }, +); + +const Button = ({ title, onPress }: { title: string; onPress: () => void }) => ( + + {title} ); @@ -58,7 +203,8 @@ function Home({ navigation: NativeStackNavigationProp; }) { return ( - +