Skip to content
Open
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 @@ -137,6 +137,11 @@ class TabsHostViewManager :
value: String?,
) = Unit

override fun setTabBarControllerMode(
view: TabsHost,
value: String?,
) = Unit

// Android additional

@ReactProp(name = "tabBarItemTitleFontColorActive", customType = "Color")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
case "tabBarMinimizeBehavior":
mViewManager.setTabBarMinimizeBehavior(view, (String) value);
break;
case "tabBarControllerMode":
mViewManager.setTabBarControllerMode(view, (String) value);
break;
case "controlNavigationStateInJS":
mViewManager.setControlNavigationStateInJS(view, value == null ? false : (boolean) value);
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ public interface RNSBottomTabsManagerInterface<T extends View> {
void setTabBarItemLabelVisibilityMode(T view, @Nullable String value);
void setTabBarTintColor(T view, @Nullable Integer value);
void setTabBarMinimizeBehavior(T view, @Nullable String value);
void setTabBarControllerMode(T view, @Nullable String value);
void setControlNavigationStateInJS(T view, boolean value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BottomTabsScreen,
BottomTabsScreenProps,
NativeFocusChangeEvent,
TabBarControllerMode,
} from 'react-native-screens';
import { Colors } from '../../../styling/Colors';
import ConfigWrapperContext from './ConfigWrapperContext';
Expand All @@ -16,6 +17,7 @@ export interface TabConfiguration {

export interface BottomTabsContainerProps {
tabConfigs: TabConfiguration[];
tabBarControllerMode?: TabBarControllerMode | undefined;
}

export function BottomTabsContainer(props: BottomTabsContainerProps) {
Expand Down Expand Up @@ -90,6 +92,7 @@ export function BottomTabsContainer(props: BottomTabsContainerProps) {
tabBarItemTitleFontWeight="700"
tabBarItemLabelVisibilityMode="auto"
tabBarMinimizeBehavior="onScrollDown"
tabBarControllerMode={props.tabBarControllerMode ?? 'automatic'}
experimentalControlNavigationStateInJS={
configWrapper.config.controlledBottomTabs
}>
Expand All @@ -109,7 +112,7 @@ export function BottomTabsContainer(props: BottomTabsContainerProps) {
{...tabConfig.tabScreenProps}
isFocused={isFocused} // notice that the value passed by user is overriden here!
>
<ContentComponent/>
<ContentComponent />
</BottomTabsScreen>
);
})}
Expand Down
96 changes: 96 additions & 0 deletions apps/src/tests/Test3236.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, { useState } from 'react';

import { enableFreeze, TabBarControllerMode } from 'react-native-screens';
import ConfigWrapperContext, {
type Configuration,
DEFAULT_GLOBAL_CONFIGURATION,
} from '../shared/gamma/containers/bottom-tabs/ConfigWrapperContext';
import {
BottomTabsContainer,
type TabConfiguration,
} from '../shared/gamma/containers/bottom-tabs/BottomTabsContainer';
import { CenteredLayoutView } from '../shared/CenteredLayoutView';
import { Text } from 'react-native';
import { Button } from '../shared';

enableFreeze(true);

function makeTab(
title: string,
controllerMode: TabBarControllerMode,
setControllerMode: (mode: TabBarControllerMode) => void,
) {
return function Tab() {
return (
<CenteredLayoutView>
<Text>{title}</Text>
<Button
title={`Change mode (currently ${controllerMode})`}
onPress={() => {
switch (controllerMode) {
case 'automatic':
setControllerMode('tabBar');
break;
case 'tabBar':
setControllerMode('tabSidebar');
break;
default:
setControllerMode('automatic');
break;
}
}}
/>
</CenteredLayoutView>
);
};
}

function App() {
const [config, setConfig] = React.useState<Configuration>(
DEFAULT_GLOBAL_CONFIGURATION,
);

const [controllerMode, setControllerMode] =
useState<TabBarControllerMode>('automatic');

const TAB_CONFIGS: TabConfiguration[] = [
{
tabScreenProps: {
tabKey: 'Tab1',
title: 'Tab 1',
freezeContents: false,
icon: {
sfSymbolName: 'sun.max',
},
iconResourceName: 'sunny',
},
component: makeTab('Tab 1', controllerMode, setControllerMode),
},
{
tabScreenProps: {
tabKey: 'Tab2',
title: 'Tab 2',
icon: {
sfSymbolName: 'snow',
},
iconResourceName: 'mode_cool',
},
component: makeTab('Tab 2', controllerMode, setControllerMode),
},
];

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

export default App;
1 change: 1 addition & 0 deletions apps/src/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export { default as Test3111 } from './Test3111';
export { default as Test3115 } from './Test3115';
export { default as Test3168 } from './Test3168';
export { default as Test3173 } from './Test3173';
export { default as Test3236 } from './Test3236';
export { default as TestScreenAnimation } from './TestScreenAnimation';
export { default as TestScreenAnimationV5 } from './TestScreenAnimationV5';
export { default as TestHeader } from './TestHeader';
Expand Down
8 changes: 8 additions & 0 deletions ios/RNSEnums.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,14 @@ typedef NS_ENUM(NSInteger, RNSTabBarMinimizeBehavior) {
};
#endif

#if !RCT_NEW_ARCH_ENABLED
typedef NS_ENUM(NSInteger, RNSTabBarControllerMode) {
RNSTabBarControllerModeAutomatic,
RNSTabBarControllerModeTabBar,
RNSTabBarControllerModeTabSidebar,
};
#endif

// TODO: investigate objc - swift interop and deduplicate this code
// This enum needs to be compatible with the RNSOrientationSwift enum.
typedef NS_ENUM(NSInteger, RNSOrientation) {
Expand Down
10 changes: 10 additions & 0 deletions ios/bottom-tabs/RCTConvert+RNSBottomTabs.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@ + (UIOffset)UIOffset:(id)json;
RNSTabBarMinimizeBehaviorAutomatic,
integerValue)

RCT_ENUM_CONVERTER(
RNSTabBarControllerMode,
(@{
@"automatic" : @(RNSTabBarControllerModeAutomatic),
@"tabBar" : @(RNSTabBarControllerModeTabBar),
@"tabSidebar" : @(RNSTabBarControllerModeTabSidebar),
}),
RNSTabBarControllerModeAutomatic,
integerValue)

RCT_ENUM_CONVERTER(
RNSOrientation,
(@{
Expand Down
3 changes: 3 additions & 0 deletions ios/bottom-tabs/RNSBottomTabsHostComponentView.h
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) UITabBarMinimizeBehavior tabBarMinimizeBehavior API_AVAILABLE(ios(26.0));
#endif // Check for iOS >= 26

#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
@property (nonatomic, readonly) UITabBarControllerMode tabBarControllerMode API_AVAILABLE(ios(18.0));
#endif // Check for iOS >= 18
@end

#pragma mark - React Events
Expand Down
33 changes: 33 additions & 0 deletions ios/bottom-tabs/RNSBottomTabsHostComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,26 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
}
}

if (newComponentProps.tabBarControllerMode != oldComponentProps.tabBarControllerMode) {
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
_tabBarControllerMode = rnscreens::conversion::UITabBarControllerModeFromRNSBottomTabsTabBarControllerMode(
newComponentProps.tabBarControllerMode);
_controller.mode = _tabBarControllerMode;
if (_tabBarControllerMode == UITabBarControllerModeTabSidebar) {
// Also show the sidebar on iPad if the device is in landscape mode (default behavior)
UIDeviceOrientation current = [[UIDevice currentDevice] orientation];
if (UIDeviceOrientationIsLandscape(current) ) {
_controller.sidebar.hidden = false;
}
}
} else
#endif // Check for iOS >= 18
if (newComponentProps.tabBarControllerMode != react::RNSBottomTabsTabBarControllerMode::Automatic) {
RCTLogWarn(@"[RNScreens] tabBarControllerMode is supported for iOS >= 18");
}
}

// Super call updates _props pointer. We should NOT update it before calling super.
[super updateProps:props oldProps:oldProps];
}
Expand Down Expand Up @@ -432,6 +452,19 @@ - (void)setTabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior:(RNSTabBarMinimiz
}
}

- (void)setTabBarControllerModeFromRNSTabBarControllerMode:(RNSTabBarControllerMode)tabBarControllerMode
{
#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)
if (@available(iOS 18.0, *)) {
_tabBarControllerMode = rnscreens::conversion::UITabBarControllerModeFromRNSTabBarControllerMode(tabBarControllerMode);
_controller.mode = _tabBarControllerMode;
} else
#endif // Check for iOS >= 18
if (tabBarControllerMode != RNSTabBarControllerModeAutomatic) {
RCTLogWarn(@"[RNScreens] tabBarControllerMode is supported for iOS >= 18");
}
}

- (void)setOnNativeFocusChange:(RCTDirectEventBlock)onNativeFocusChange
{
[self.reactEventEmitter setOnNativeFocusChange:onNativeFocusChange];
Expand Down
6 changes: 6 additions & 0 deletions ios/bottom-tabs/RNSBottomTabsHostComponentViewManager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ - (UIView *)view
tabBarMinimizeBehavior,
tabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior,
RNSTabBarMinimizeBehavior);
// This remapping allows us to store UITabBarControllerMode in the component while accepting a custom enum as input
// from JS.
RCT_REMAP_VIEW_PROPERTY(
tabBarControllerMode,
tabBarControllerModeFromRNSTabBarControllerMode,
RNSTabBarControllerMode);
Comment on lines +34 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is necessary. You could most likely get rid of whole RNSTabBarControllerMode enum, simply by adding RCTCovnert implementation for UITabBarControllerMode. This should be possible.

I see now, that we do similar thing above & you followed what's already there. That's fine then. @kligarski would you take a look at what I'm saying later in the week?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we can probably ignore the prop completely if we are using SDK < 18/26. I added an issue to our internal board: https://github.com/software-mansion/react-native-screens-labs/issues/471 and I'll have a look at it after this PR is merged.


// TODO: Missing prop
//@property (nonatomic, readonly) BOOL experimental_controlNavigationStateInJS;
Expand Down
40 changes: 40 additions & 0 deletions ios/conversion/RNSConversions-BottomTabs.mm
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,46 @@ UITabBarMinimizeBehavior UITabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior(

#endif // Check for iOS >= 26

#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)

#if RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(18.0))
UITabBarControllerMode UITabBarControllerModeFromRNSBottomTabsTabBarControllerMode(
react::RNSBottomTabsTabBarControllerMode tabBarControllerMode)
{
using enum facebook::react::RNSBottomTabsTabBarControllerMode;

switch (tabBarControllerMode) {
case Automatic:
return UITabBarControllerModeAutomatic;
case TabBar:
return UITabBarControllerModeTabBar;
case TabSidebar:
return UITabBarControllerModeTabSidebar;
default:
return UITabBarControllerModeAutomatic;
}
}
#else // RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(18.0))
UITabBarControllerMode UITabBarControllerModeFromRNSTabBarControllerMode(
RNSTabBarControllerMode tabBarDisplayMode)
{
switch (tabBarDisplayMode) {
case RNSTabBarControllerModeAutomatic:
return UITabBarControllerModeAutomatic;
case RNSTabBarControllerModeTabBar:
return UITabBarControllerModeTabBar;
case RNSTabBarControllerModeTabSidebar:
return UITabBarControllerModeTabSidebar;
default:
return UITabBarControllerModeAutomatic;
}
}
#endif // RCT_NEW_ARCH_ENABLED

#endif // Check for iOS >= 18

RNSBottomTabsIconType RNSBottomTabsIconTypeFromIcon(react::RNSBottomTabsScreenIconType iconType)
{
using enum facebook::react::RNSBottomTabsScreenIconType;
Expand Down
14 changes: 14 additions & 0 deletions ios/conversion/RNSConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ UITabBarMinimizeBehavior UITabBarMinimizeBehaviorFromRNSTabBarMinimizeBehavior(

#endif // Check for iOS >= 26

#if RNS_IPHONE_OS_VERSION_AVAILABLE(18_0)

#if RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(18.0))
UITabBarControllerMode UITabBarControllerModeFromRNSBottomTabsTabBarControllerMode(
react::RNSBottomTabsTabBarControllerMode tabBarControllerMode);
#else // RCT_NEW_ARCH_ENABLED
API_AVAILABLE(ios(18.0))
UITabBarControllerMode UITabBarControllerModeFromRNSTabBarControllerMode(
RNSTabBarControllerMode tabBarControllerMode);
#endif // RCT_NEW_ARCH_ENABLED

#endif // Check for iOS >= 18

RNSBottomTabsIconType RNSBottomTabsIconTypeFromIcon(react::RNSBottomTabsScreenIconType iconType);

RNSBottomTabsScreenSystemItem RNSBottomTabsScreenSystemItemFromReactRNSBottomTabsScreenSystemItem(
Expand Down
27 changes: 27 additions & 0 deletions src/components/bottom-tabs/BottomTabs.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ export type TabBarMinimizeBehavior =
| 'onScrollDown'
| 'onScrollUp';

// iOS-specific
export type TabBarControllerMode =
| 'automatic'
| 'tabBar'
| 'tabSidebar';

export interface BottomTabsProps extends ViewProps {
// #region Events
/**
Expand Down Expand Up @@ -180,6 +186,27 @@ export interface BottomTabsProps extends ViewProps {
* @supported iOS 26 or higher
*/
tabBarMinimizeBehavior?: TabBarMinimizeBehavior;
/**
* @summary Specifies the display mode for the tab bar.
*
* Available starting from iOS 18.
* Not supported on tvOS.
*
* The following values are currently supported:
*
* - `automatic` - the system sets the display mode based on the tab’s content
* - `tabBar` - the system displays the content only as a tab bar
* - `tabSidebar` - the tab bar is displayed as a sidebar
*
* See the official documentation for more details:
* @see {@link https://developer.apple.com/documentation/uikit/uitabbarcontroller/mode|UITabBarController.Mode}
*
* @default Defaults to `automatic`.
*
* @platform ios
* @supported iOS 18 or higher
*/
tabBarControllerMode?: TabBarControllerMode;
// #endregion iOS-only appearance

// #region Experimental support
Expand Down
6 changes: 6 additions & 0 deletions src/fabric/bottom-tabs/BottomTabsNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ type TabBarMinimizeBehavior =
| 'onScrollDown'
| 'onScrollUp';

type TabBarControllerMode =
| 'automatic'
| 'tabBar'
| 'tabSidebar';

export interface NativeProps extends ViewProps {
// Events
onNativeFocusChange?: DirectEventHandler<NativeFocusChangeEvent>;
Expand Down Expand Up @@ -61,6 +66,7 @@ export interface NativeProps extends ViewProps {
// iOS-specific
tabBarTintColor?: ColorValue;
tabBarMinimizeBehavior?: WithDefault<TabBarMinimizeBehavior, 'automatic'>;
tabBarControllerMode?: WithDefault<TabBarControllerMode, 'automatic'>;

// Control

Expand Down