Skip to content

Commit 896c2c6

Browse files
committed
feat(iOS): native bottom tabs PoC (#1)
* Setup component skeleton * Add playground app * Add pragma marks * Add container update code * Add second tab to example * Add custom RNSTabBarController * Add some logic & make tab bar switching work * Add some logging * Allow updateBounds to update shadow state for tab screens * Print some logs on tab change * Add temporarily styles to BottomTabs view * Update example * Move view manager to dedicated file * Add RNSBottomTabScreen native component * Migrate to dedicated RNSBottomTabScreen * Fix typing * Fix layout & update container synchronously after receiving react updates * Rename RNSBottomTabsView -> RNSBottomTabsHostComponentView * Small cleanup * Expose customisation of background color * Rename RNSBottomTabScreenComponentView -> RNSBottomTabsScreenComponentView
1 parent fba23e1 commit 896c2c6

22 files changed

+535
-5
lines changed

apps/App.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React from 'react';
22
import { enableFreeze } from 'react-native-screens';
3-
import Example from './Example';
4-
//import * as Test from './src/tests';
3+
// import Example from './Example';
4+
import * as Test from './src/tests';
55

66
enableFreeze(true);
77

88
export default function App() {
9-
return <Example />;
10-
//return <Test.Test42 />;
9+
// return <Example />;
10+
return <Test.TestBottomTabs />;
1111
}

apps/src/tests/TestBottomTabs.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React, { PropsWithChildren } from 'react';
2+
import { Text, View, ViewProps } from 'react-native';
3+
4+
import { BottomTabs, BottomTabsScreen } from 'react-native-screens';
5+
6+
function LayoutView(props: PropsWithChildren<ViewProps>) {
7+
const { children, style, ...rest } = props;
8+
9+
return (
10+
<View style={[{ flex: 1, width: '100%', height: '100%', justifyContent: 'center', alignItems: 'center' }, style]} {...rest}>
11+
{children}
12+
</View>
13+
);
14+
}
15+
16+
function App() {
17+
return (
18+
<View style={{ flex: 1 }}>
19+
<BottomTabs tabBarBackgroundColor={'yellow'}>
20+
<BottomTabsScreen>
21+
<LayoutView style={{ backgroundColor: 'lightgreen' }}>
22+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
23+
<Text>Hello world from native bottom tab</Text>
24+
</View>
25+
</LayoutView>
26+
</BottomTabsScreen>
27+
<BottomTabsScreen>
28+
<LayoutView style={{ backgroundColor: 'lightblue' }}>
29+
<Text>Tab2 world from native bottom tab</Text>
30+
</LayoutView>
31+
</BottomTabsScreen>
32+
<BottomTabsScreen>
33+
<LayoutView style={{ backgroundColor: 'yellow' }}>
34+
<Text>Tab3 world from native bottom tab</Text>
35+
</LayoutView>
36+
</BottomTabsScreen>
37+
</BottomTabs>
38+
</View>
39+
);
40+
}
41+
42+
export default App;

apps/src/tests/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,5 @@ export { default as TestMemoryLeak } from './TestMemoryLeak';
152152
export { default as TestFormSheet } from './TestFormSheet';
153153
export { default as TestAndroidTransitions } from './TestAndroidTransitions';
154154
export { default as TestAnimation } from './TestAnimation';
155+
export { default as TestBottomTabs } from './TestBottomTabs';
155156

common/cpp/react/renderer/components/rnscreens/RNSScreenShadowNode.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ void RNSScreenShadowNode::appendChild(const ShadowNode::Shared &child) {
118118
void RNSScreenShadowNode::layout(facebook::react::LayoutContext layoutContext) {
119119
YogaLayoutableShadowNode::layout(layoutContext);
120120

121+
std::printf(
122+
"ScreenSN [%d] layout {{%.2lf, %.2lf}, {%.2lf, %.2lf}}\n",
123+
getTag(),
124+
layoutMetrics_.frame.origin.x,
125+
layoutMetrics_.frame.origin.y,
126+
layoutMetrics_.frame.size.width,
127+
layoutMetrics_.frame.size.height);
128+
121129
#ifdef ANDROID
122130
applyFrameCorrections();
123131
#endif // ANDROID

ios/RNSScreen.mm

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#import "RNSScreenFooter.h"
3232
#import "RNSScreenStack.h"
3333
#import "RNSScreenStackHeaderConfig.h"
34+
#import "RNSTabBarController.h"
3435

3536
#import "RNSDefines.h"
3637
#import "UIView+RNSUtility.h"
@@ -1164,6 +1165,18 @@ - (void)updateFormSheetPresentationStyle
11641165

11651166
#endif // !TARGET_OS_TV && !TARGET_OS_VISION
11661167

1168+
- (void)setFrame:(CGRect)frame
1169+
{
1170+
NSLog(@"ScreenView [%ld] setFrame: %@", self.tag, NSStringFromCGRect(frame));
1171+
[super setFrame:frame];
1172+
}
1173+
1174+
- (void)setBounds:(CGRect)bounds
1175+
{
1176+
NSLog(@"ScreenView [%ld] setBounds: %@", self.tag, NSStringFromCGRect(bounds));
1177+
[super setBounds:bounds];
1178+
}
1179+
11671180
#pragma mark - Fabric specific
11681181
#ifdef RCT_NEW_ARCH_ENABLED
11691182

@@ -1551,13 +1564,14 @@ - (void)viewDidLayoutSubviews
15511564
// shown as a native modal, as the final dimensions of the modal on iOS 12+ are shorter than the
15521565
// screen size
15531566
BOOL isDisplayedWithinUINavController = [self.parentViewController isKindOfClass:[RNSNavigationController class]];
1567+
BOOL isTabScreen = [self.parentViewController isKindOfClass:RNSTabBarController.class];
15541568

15551569
// Calculate header height on modal open
15561570
if (self.screenView.isPresentedAsNativeModal) {
15571571
[self calculateAndNotifyHeaderHeightChangeIsModal:YES];
15581572
}
15591573

1560-
if (isDisplayedWithinUINavController || self.screenView.isPresentedAsNativeModal) {
1574+
if (isDisplayedWithinUINavController || isTabScreen || self.screenView.isPresentedAsNativeModal) {
15611575
#ifdef RCT_NEW_ARCH_ENABLED
15621576
[self.screenView updateBounds];
15631577
#else

ios/RNSScreenContainer.mm

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
#endif // RCT_NEW_ARCH_ENABLED
1414

15+
#pragma mark - RNSViewController
16+
1517
@implementation RNSViewController
1618

1719
#if !TARGET_OS_TV
@@ -54,6 +56,8 @@ - (UIViewController *)findActiveChildVC
5456

5557
@end
5658

59+
#pragma mark - RNSScreenContainerView
60+
5761
@implementation RNSScreenContainerView {
5862
BOOL _invalidated;
5963
NSMutableSet *_activeScreens;
@@ -262,6 +266,8 @@ + (void)load
262266
[super load];
263267
}
264268

269+
#pragma mark - RCTViewComponentViewProtocol
270+
265271
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
266272
{
267273
if (![childComponentView isKindOfClass:[RNSScreenView class]]) {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#import <React/RCTViewComponentView.h>
2+
#import "RNSBottomTabsHostComponentViewManager.h"
3+
#import "RNSScreenContainer.h"
4+
5+
NS_ASSUME_NONNULL_BEGIN
6+
7+
/**
8+
* Component view. Lifecycle is managed by React Native.
9+
*
10+
* This component serves as:
11+
* 1. host for UITabBarController
12+
* 2. provider of React state & props for the tab bar controller
13+
* 3. two way communication channel with React (commands & events)
14+
*/
15+
@interface RNSBottomTabsHostComponentView : RCTViewComponentView <RNSScreenContainerDelegate>
16+
17+
#pragma mark - Props
18+
// TODO: Extract all props to single struct / use codegened struct
19+
20+
@property (nonatomic, strong, readonly, nullable) UIColor *tabBarBackgroundColor;
21+
22+
@end
23+
24+
NS_ASSUME_NONNULL_END
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
#import "RNSBottomTabsHostComponentView.h"
2+
#import <React/RCTConversions.h>
3+
#import <React/RCTMountingTransactionObserving.h>
4+
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
5+
#import <react/renderer/components/rnscreens/EventEmitters.h>
6+
#import <react/renderer/components/rnscreens/Props.h>
7+
#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h>
8+
#import "RNSBottomTabsScreenComponentView.h"
9+
#import "RNSDefines.h"
10+
#import "RNSTabBarController.h"
11+
12+
namespace react = facebook::react;
13+
14+
#pragma mark - View implementation
15+
16+
@interface RNSBottomTabsHostComponentView () <RCTMountingTransactionObserving>
17+
@end
18+
19+
@implementation RNSBottomTabsHostComponentView {
20+
RNSTabBarController *_controller;
21+
22+
// RCTViewComponentView does not expose this field, therefore we maintain
23+
// it on our side.
24+
NSMutableArray<RNSBottomTabsScreenComponentView *> *_reactSubviews;
25+
BOOL _hasModifiedReactSubviewsInCurrentTransaction;
26+
BOOL _needsTabBarAppearanceUpdate;
27+
}
28+
29+
- (instancetype)initWithFrame:(CGRect)frame
30+
{
31+
if (self = [super initWithFrame:frame]) {
32+
[self initState];
33+
}
34+
return self;
35+
}
36+
37+
- (void)initState
38+
{
39+
static const auto defaultProps = std::make_shared<const react::RNSBottomTabsProps>();
40+
_props = defaultProps;
41+
_controller = [[RNSTabBarController alloc] init];
42+
_reactSubviews = [NSMutableArray new];
43+
_hasModifiedReactSubviewsInCurrentTransaction = NO;
44+
_needsTabBarAppearanceUpdate = NO;
45+
}
46+
47+
- (nullable UITabBarAppearance *)makeTabBarAppearance
48+
{
49+
UITabBarAppearance *newAppearance = [[UITabBarAppearance alloc] init];
50+
newAppearance.backgroundColor = _tabBarBackgroundColor;
51+
return newAppearance;
52+
}
53+
54+
#pragma mark - UIView methods
55+
56+
- (void)didMoveToWindow
57+
{
58+
[self reactAddControllerToClosestParent:_controller];
59+
}
60+
61+
- (void)reactAddControllerToClosestParent:(UIViewController *)controller
62+
{
63+
if (!controller.parentViewController) {
64+
UIView *parentView = (UIView *)self.reactSuperview;
65+
while (parentView) {
66+
if (parentView.reactViewController) {
67+
[parentView.reactViewController addChildViewController:controller];
68+
[self addSubview:controller.view];
69+
#if !TARGET_OS_TV
70+
#endif
71+
[controller didMoveToParentViewController:parentView.reactViewController];
72+
break;
73+
}
74+
parentView = (UIView *)parentView.reactSuperview;
75+
}
76+
return;
77+
}
78+
}
79+
80+
#pragma mark - RNSScreenContainerDelegate
81+
82+
- (void)updateContainer
83+
{
84+
NSMutableArray<UIViewController *> *tabControllers = [[NSMutableArray alloc] initWithCapacity:_reactSubviews.count];
85+
for (RNSBottomTabsScreenComponentView *childView in _reactSubviews) {
86+
[tabControllers addObject:childView.controller];
87+
}
88+
89+
NSLog(@"updateContainer: tabControllers: %@", tabControllers);
90+
91+
[_controller setViewControllers:tabControllers animated:NO];
92+
[[_controller tabBar] setItemPositioning:UITabBarItemPositioningCentered];
93+
NSLog(@"updateContainer: tabBarItems %@", [[_controller tabBar] items]);
94+
for (UITabBarItem *tabBarItem in [[_controller tabBar] items]) {
95+
tabBarItem.badgeValue = @"Hello!";
96+
}
97+
}
98+
99+
- (void)markChildUpdated
100+
{
101+
[self updateContainer];
102+
}
103+
104+
#pragma mark - RCTViewComponentViewProtocol
105+
106+
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
107+
{
108+
RCTAssert(
109+
[childComponentView isKindOfClass:RNSBottomTabsScreenComponentView.class],
110+
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to mount %@",
111+
childComponentView);
112+
113+
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(childComponentView);
114+
childScreen.reactSuperview = self;
115+
116+
[_reactSubviews insertObject:childScreen atIndex:index];
117+
_hasModifiedReactSubviewsInCurrentTransaction = YES;
118+
}
119+
120+
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
121+
{
122+
RCTAssert(
123+
[childComponentView isKindOfClass:RNSBottomTabsScreenComponentView.class],
124+
@"BottomTabsView only accepts children of type BottomTabScreen. Attempted to unmount %@",
125+
childComponentView);
126+
127+
auto *childScreen = static_cast<RNSBottomTabsScreenComponentView *>(childComponentView);
128+
childScreen.reactSuperview = nil;
129+
130+
[_reactSubviews removeObject:childScreen];
131+
_hasModifiedReactSubviewsInCurrentTransaction = YES;
132+
}
133+
134+
- (void)updateProps:(const facebook::react::Props::Shared &)props
135+
oldProps:(const facebook::react::Props::Shared &)oldProps
136+
{
137+
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsProps>(_props);
138+
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsProps>(props);
139+
140+
if (newComponentProps.tabBarBackgroundColor != oldComponentProps.tabBarBackgroundColor) {
141+
_needsTabBarAppearanceUpdate = YES;
142+
_tabBarBackgroundColor = RCTUIColorFromSharedColor(newComponentProps.tabBarBackgroundColor);
143+
}
144+
145+
// Super call updates _props pointer. We should NOT update it before calling super.
146+
[super updateProps:props oldProps:oldProps];
147+
}
148+
149+
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
150+
{
151+
if (_needsTabBarAppearanceUpdate) {
152+
_needsTabBarAppearanceUpdate = NO;
153+
[_controller applyTabBarAppearance:[self makeTabBarAppearance]];
154+
}
155+
[super finalizeUpdates:updateMask];
156+
}
157+
158+
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
159+
{
160+
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsComponentDescriptor>();
161+
}
162+
163+
#pragma mark - RCTMountingTransactionObserving
164+
165+
- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction
166+
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
167+
{
168+
_hasModifiedReactSubviewsInCurrentTransaction = NO;
169+
}
170+
171+
- (void)mountingTransactionDidMount:(const facebook::react::MountingTransaction &)transaction
172+
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
173+
{
174+
if (_hasModifiedReactSubviewsInCurrentTransaction) {
175+
[self updateContainer];
176+
}
177+
}
178+
179+
@end
180+
181+
#pragma mark - View class exposure
182+
183+
Class<RCTComponentViewProtocol> RNSBottomTabsCls(void)
184+
{
185+
return RNSBottomTabsHostComponentView.class;
186+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#pragma once
2+
3+
#import <React/RCTViewManager.h>
4+
5+
NS_ASSUME_NONNULL_BEGIN
6+
7+
@interface RNSBottomTabsHostComponentViewManager : RCTViewManager
8+
9+
@end
10+
11+
NS_ASSUME_NONNULL_END
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#import "RNSBottomTabsHostComponentViewManager.h"
2+
3+
@implementation RNSBottomTabsHostComponentViewManager
4+
5+
RCT_EXPORT_MODULE(RNSBottomTabsViewManager)
6+
7+
@end

0 commit comments

Comments
 (0)