Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -340,4 +340,11 @@ class ScreenStackHeaderConfigViewManager :
) {
logNotAvailable("headerRightBarButtonItems")
}

override fun setUserInterfaceStyle(
view: ScreenStackHeaderConfig?,
value: String?,
) {
logNotAvailable("userInterfaceStyle")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ class TabScreenViewManager :
value: String?,
) = Unit

override fun setUserInterfaceStyle(
view: TabScreen,
value: String?,
) = Unit

@ReactProp(name = "imageIconResource")
override fun setImageIconResource(
view: TabScreen,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "topScrollEdgeEffect":
mViewManager.setTopScrollEdgeEffect(view, (String) value);
break;
case "userInterfaceStyle":
mViewManager.setUserInterfaceStyle(view, (String) value);
break;
default:
super.setProperty(view, propName, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ public interface RNSBottomTabsScreenManagerInterface<T extends View> {
void setLeftScrollEdgeEffect(T view, @Nullable String value);
void setRightScrollEdgeEffect(T view, @Nullable String value);
void setTopScrollEdgeEffect(T view, @Nullable String value);
void setUserInterfaceStyle(T view, @Nullable String value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "synchronousShadowStateUpdatesEnabled":
mViewManager.setSynchronousShadowStateUpdatesEnabled(view, value == null ? false : (boolean) value);
break;
case "userInterfaceStyle":
mViewManager.setUserInterfaceStyle(view, (String) value);
break;
default:
super.setProperty(view, propName, value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ public interface RNSScreenStackHeaderConfigManagerInterface<T extends View> {
void setHeaderLeftBarButtonItems(T view, @Nullable ReadableArray value);
void setHeaderRightBarButtonItems(T view, @Nullable ReadableArray value);
void setSynchronousShadowStateUpdatesEnabled(T view, boolean value);
void setUserInterfaceStyle(T view, @Nullable String value);
}
153 changes: 153 additions & 0 deletions apps/src/tests/Test3342.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React from 'react';
import {
DefaultTheme,
NavigationContainer,
ParamListBase,
} from '@react-navigation/native';
import {
NativeStackNavigationProp,
createNativeStackNavigator,
} from '@react-navigation/native-stack';
import { Button, StyleSheet, Text, TextInput, View } from 'react-native';
import ConfigWrapperContext, {
Configuration,
DEFAULT_GLOBAL_CONFIGURATION,
} from '../shared/gamma/containers/bottom-tabs/ConfigWrapperContext';
import {
BottomTabsContainer,
TabConfiguration,
} from '../shared/gamma/containers/bottom-tabs/BottomTabsContainer';
import Colors from '../shared/styling/Colors';

type RouteParamList = {
Screen1: undefined;
Screen2: undefined;
Screen3: undefined;
};

type NavigationProp<ParamList extends ParamListBase> = {
navigation: NativeStackNavigationProp<ParamList>;
};

type StackNavigationProp = NavigationProp<RouteParamList>;

const Stack = createNativeStackNavigator<RouteParamList>();

function Screen1({ navigation }: StackNavigationProp) {
return (
<View>
<Text>Enable system dark mode and observe the back button.</Text>
<Button
title="Go to screen 2"
onPress={() => navigation.push('Screen2')}
/>
</View>
);
}

function TabScreen({ navigation }: StackNavigationProp) {
return (
<View style={{ flex: 1, backgroundColor: Colors.cardBackground }}>
<Button
title="Go to screen 3"
onPress={() => navigation.push('Screen3')}
/>
<TextInput
style={styles.input}
placeholder="Test keyboard appearance..."
placeholderTextColor="gray"
/>
</View>
);
}

function Screen2(stackNavProp: StackNavigationProp) {
const [config, setConfig] = React.useState<Configuration>(
DEFAULT_GLOBAL_CONFIGURATION,
);

const TAB_CONFIGS: TabConfiguration[] = [
{
tabScreenProps: {
tabKey: 'Tab1',
title: 'Tab 1',
icon: {
ios: {
type: 'sfSymbol',
name: 'house',
},
},
experimental_userInterfaceStyle: 'light',
},
component: () => TabScreen(stackNavProp),
},
{
tabScreenProps: {
tabKey: 'Tab2',
title: 'Tab 2',
icon: {
ios: {
type: 'sfSymbol',
name: 'rectangle.stack',
},
},
},
component: () => TabScreen(stackNavProp),
},
];

return (
<ConfigWrapperContext.Provider
value={{
config,
setConfig,
}}>
<BottomTabsContainer tabConfigs={TAB_CONFIGS} />
</ConfigWrapperContext.Provider>
);
}

function Screen3() {
return (
<View>
<TextInput
style={styles.input}
placeholder="Test keyboard appearance..."
placeholderTextColor="gray"
/>
</View>
);
}

export default function App() {
return (
<NavigationContainer theme={DefaultTheme}>
<Stack.Navigator
screenOptions={{
statusBarStyle: 'dark',
experimental_userInterfaceStyle: 'light',
}}>
<Stack.Screen name="Screen1" component={Screen1} />
<Stack.Screen
name="Screen2"
component={Screen2}
options={{ headerBackTitle: 'Screen1' }}
/>
<Stack.Screen
name="Screen3"
component={Screen3}
options={{ headerBackTitle: 'Screen2' }}
/>
</Stack.Navigator>
</NavigationContainer>
);
}

const styles = StyleSheet.create({
input: {
height: 40,
margin: 12,
borderWidth: 1,
padding: 10,
},
});
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ export { default as Test3239 } from './Test3239';
export { default as Test3265 } from './Test3265';
export { default as Test3271 } from './Test3271';
export { default as Test3282 } from './Test3282';
export { default as Test3342 } from './Test3342';
export { default as Test3369 } from './Test3369';
export { default as TestScreenAnimation } from './TestScreenAnimation';
export { default as TestScreenAnimationV5 } from './TestScreenAnimationV5';
Expand Down
14 changes: 14 additions & 0 deletions guides/GUIDE_FOR_LIBRARY_AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,20 @@ A flag to that lets you opt out of insetting the header. You may want to set thi

When set to true, it makes native navigation bar semi transparent. It adds blur effect on iOS. The default value is false.

### `experimental_userInterfaceStyle` (iOS only)

Allows to override system appearance for the navigation bar. Does not support dynamic changes to the prop value for the currently visible screen. The default value is `unspecified`.

Please note that this prop is marked as **experimental** and might be subject to breaking changes or even removal.

The following values are currently supported:

- `unspecified` - an unspecified interface style,
- `light` - the light interface style,
- `dark` - the dark interface style.

The supported values correspond to the official UIKit documentation: https://developer.apple.com/documentation/uikit/uiuserinterfacestyle.

# Guide for native component authors

If you are adding a new native component to be used from the React Native app, you may want it to respond to navigation lifecycle events.
Expand Down
3 changes: 3 additions & 0 deletions ios/RNSConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ namespace react = facebook::react;
+ (RNSOptionalBoolean)RNSOptionalBooleanFromRNSSearchBarHideNavigationBar:
(react::RNSSearchBarHideNavigationBar)hideNavigationBar;

+ (UIUserInterfaceStyle)UIUserInterfaceStyleFromCppEquivalent:
(react::RNSScreenStackHeaderConfigUserInterfaceStyle)userInterfaceStyle;

+ (NSMutableArray<NSNumber *> *)arrayFromVector:(const std::vector<CGFloat> &)vector;

+ (RNSBlurEffectStyle)RNSBlurEffectStyleFromCppEquivalent:(react::RNSScreenStackHeaderConfigBlurEffect)blurEffect;
Expand Down
17 changes: 17 additions & 0 deletions ios/RNSConvert.mm
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,23 @@ + (RNSOptionalBoolean)RNSOptionalBooleanFromRNSSearchBarHideNavigationBar:
}
}

+ (UIUserInterfaceStyle)UIUserInterfaceStyleFromCppEquivalent:
(react::RNSScreenStackHeaderConfigUserInterfaceStyle)userInterfaceStyle
{
switch (userInterfaceStyle) {
using enum react::RNSScreenStackHeaderConfigUserInterfaceStyle;

case Unspecified:
return UIUserInterfaceStyleUnspecified;
case Light:
return UIUserInterfaceStyleLight;
case Dark:
return UIUserInterfaceStyleDark;
default:
RCTLogError(@"[RNScreens] unsupported user interface style");
}
}

+ (NSMutableArray<NSNumber *> *)arrayFromVector:(const std::vector<CGFloat> &)vector
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:vector.size()];
Expand Down
13 changes: 13 additions & 0 deletions ios/RNSScreenStackHeaderConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,24 @@ NS_ASSUME_NONNULL_END

@end

#pragma mark - Experimental

@interface RNSScreenStackHeaderConfig ()

@property (nonatomic) UIUserInterfaceStyle userInterfaceStyle;

@end

#pragma mark - View Manager

@interface RNSScreenStackHeaderConfigManager : RCTViewManager

@end

#ifdef RCT_NEW_ARCH_ENABLED
#else

#pragma mark - Legacy Shadow View
/**
* Used as local data send to shadow view on Paper. This helps us to provide Yoga
* with knowledge of native insets in the navigation bar.
Expand All @@ -155,6 +166,8 @@ NS_ASSUME_NONNULL_END
@end
#endif

#pragma mark - RCTConvert

@interface RCTConvert (RNSScreenStackHeader)

+ (UISemanticContentAttribute)UISemanticContentAttribute:(nonnull id)json;
Expand Down
7 changes: 7 additions & 0 deletions ios/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@ + (void)updateViewController:(UIViewController *)vc
return;
}

navctr.navigationBar.overrideUserInterfaceStyle = config.userInterfaceStyle;

#if !TARGET_OS_TV
[config configureBackItem:prevItem withPrevVC:prevVC];

Expand Down Expand Up @@ -1139,6 +1141,10 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props::
_backButtonDisplayMode =
[RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode];

if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) {
_userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle];
}

if (newScreenProps.direction != oldScreenProps.direction) {
_direction = [RNSConvert UISemanticContentAttributeFromCppEquivalent:newScreenProps.direction];
}
Expand Down Expand Up @@ -1287,6 +1293,7 @@ - (RCTShadowView *)shadowView
RCT_EXPORT_VIEW_PROPERTY(backButtonInCustomView, BOOL)
RCT_EXPORT_VIEW_PROPERTY(disableBackButtonMenu, BOOL)
RCT_EXPORT_VIEW_PROPERTY(backButtonDisplayMode, UINavigationItemBackButtonDisplayMode)
RCT_EXPORT_VIEW_PROPERTY(userInterfaceStyle, UIUserInterfaceStyle)
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)
RCT_EXPORT_VIEW_PROPERTY(headerLeftBarButtonItems, NSArray)
Expand Down
8 changes: 8 additions & 0 deletions ios/bottom-tabs/RNSBottomTabsScreenComponentView.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ NS_ASSUME_NONNULL_BEGIN

@end

#pragma mark - Experimental

@interface RNSBottomTabsScreenComponentView ()

@property (nonatomic) UIUserInterfaceStyle userInterfaceStyle;

@end

#pragma mark - Events

@interface RNSBottomTabsScreenComponentView ()
Expand Down
7 changes: 7 additions & 0 deletions ios/bottom-tabs/RNSBottomTabsScreenComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ - (void)resetProps
_selectedIconSfSymbolName = nil;

_systemItem = RNSBottomTabsScreenSystemItemNone;

_userInterfaceStyle = UIUserInterfaceStyleUnspecified;
}

RNS_IGNORE_SUPER_CALL_BEGIN
Expand Down Expand Up @@ -402,6 +404,11 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
scrollEdgeEffectsNeedUpdate = YES;
}

if (newComponentProps.userInterfaceStyle != oldComponentProps.userInterfaceStyle) {
_userInterfaceStyle = rnscreens::conversion::UIUserInterfaceStyleFromBottomTabsScreenCppEquivalent(
newComponentProps.userInterfaceStyle);
}

if (tabBarItemNeedsRecreation) {
[self createTabBarItem];
tabBarItemNeedsUpdate = YES;
Expand Down
2 changes: 2 additions & 0 deletions ios/bottom-tabs/RNSBottomTabsScreenComponentViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ - (UIView *)view
RCT_EXPORT_VIEW_PROPERTY(rightScrollEdgeEffect, RNSScrollEdgeEffect);
RCT_EXPORT_VIEW_PROPERTY(topScrollEdgeEffect, RNSScrollEdgeEffect);

RCT_EXPORT_VIEW_PROPERTY(userInterfaceStyle, UIUserInterfaceStyle);

RCT_EXPORT_VIEW_PROPERTY(systemItem, RNSBottomTabsScreenSystemItem);

RCT_EXPORT_VIEW_PROPERTY(onWillAppear, RCTDirectEventBlock);
Expand Down
8 changes: 8 additions & 0 deletions ios/bottom-tabs/RNSTabBarController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ - (void)updateSelectedViewController

RNSLog(@"Change selected view controller to: %@", selectedViewController);

if (@available(iOS 26.0, *)) {
// On iOS 26, we need to set user interface style 2 parent views above the tab bar
// for this prop to take effect.
self.tabBar.superview.superview.overrideUserInterfaceStyle =
selectedViewController.tabScreenComponentView.userInterfaceStyle;
} else {
self.tabBar.overrideUserInterfaceStyle = selectedViewController.tabScreenComponentView.userInterfaceStyle;
}
[selectedViewController.tabScreenComponentView overrideScrollViewBehaviorInFirstDescendantChainIfNeeded];
[selectedViewController.tabScreenComponentView updateContentScrollViewEdgeEffectsIfExists];
[self setSelectedViewController:selectedViewController];
Expand Down
Loading
Loading