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
1 change: 1 addition & 0 deletions packages/rn-tester/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def pods(target_name, options = {})
# RNTester native modules and components
pod 'ScreenshotManager', :path => "NativeModuleExample"
pod 'MyNativeView', :path => "NativeComponentExample"
pod 'RNTesterTabsIOS', :path => "RNTesterTabsIOS"
pod 'NativeCxxModuleExample', :path => "NativeCxxModuleExample"
end

Expand Down
35 changes: 34 additions & 1 deletion packages/rn-tester/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2645,6 +2645,35 @@ PODS:
- React-perflogger (= 1000.0.0)
- React-utils (= 1000.0.0)
- SocketRocket
- RNTesterTabsIOS (0.87.0-main):
- boost
- DoubleConversion
- fast_float
- fmt
- glog
- hermes-engine
- RCT-Folly
- RCT-Folly/Fabric
- RCTRequired
- RCTTypeSafety
- React-bridging
- React-Core
- React-cxxstableapi
- React-debug
- React-Fabric
- React-featureflags
- React-graphics
- React-ImageManager
- React-jsi
- React-NativeModulesApple
- React-RCTFabric
- React-renderercss
- React-rendererdebug
- React-utils
- ReactCodegen
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- ScreenshotManager (0.87.0-main):
- boost
- DoubleConversion
Expand Down Expand Up @@ -2765,6 +2794,7 @@ DEPENDENCIES:
- ReactCodegen (from `build/generated/ios/ReactCodegen`)
- ReactCommon-Samples (from `../react-native/ReactCommon/react/nativemodule/samples`)
- ReactCommon/turbomodule/core (from `../react-native/ReactCommon`)
- RNTesterTabsIOS (from `RNTesterTabsIOS`)
- ScreenshotManager (from `NativeModuleExample`)
- SocketRocket (~> 0.7.1)
- Yoga (from `../react-native/ReactCommon/yoga`)
Expand Down Expand Up @@ -2948,6 +2978,8 @@ EXTERNAL SOURCES:
:path: "../react-native/ReactCommon"
ReactCommon-Samples:
:path: "../react-native/ReactCommon/react/nativemodule/samples"
RNTesterTabsIOS:
:path: RNTesterTabsIOS
ScreenshotManager:
:path: NativeModuleExample
Yoga:
Expand Down Expand Up @@ -3041,10 +3073,11 @@ SPEC CHECKSUMS:
ReactCodegen: 19447115b4e0b8eec131ae256e7000a9a0910dc4
ReactCommon: 1376b66d6ef10d128c76c2d59fef49e706832ca6
ReactCommon-Samples: b74ec150a8210d62be6cc899d28a24e7b795edd3
RNTesterTabsIOS: a0e709192adb16ce7cd9a2485d958ee5d5b12fab
ScreenshotManager: c005d764f11c09fb4a5640a3157024dddce859e9
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
Yoga: a9afa834491f5b80bc3b10d2d91e8bfb15ab1100

PODFILE CHECKSUM: 59f1e7d58cffbfafbeb3df31151743dd4c9a52e8
PODFILE CHECKSUM: abfcf138a255569f3324d5118cecdcbc64bdb8c6

COCOAPODS: 1.16.2
3 changes: 3 additions & 0 deletions packages/rn-tester/RNTester/SceneDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ - (NSURL *)bundleURL
if (!dict[@"RNTMyNativeView"]) {
dict[@"RNTMyNativeView"] = NSClassFromString(@"RNTMyNativeViewComponentView");
}
if (!dict[@"RNTesterTabsIOS"]) {
dict[@"RNTesterTabsIOS"] = NSClassFromString(@"RNTesterTabsIOSComponentView");
}
if (!dict[@"SampleNativeComponent"]) {
dict[@"SampleNativeComponent"] = NSClassFromString(@"RCTSampleNativeComponentComponentView");
}
Expand Down
29 changes: 29 additions & 0 deletions packages/rn-tester/RNTesterTabsIOS/RNTesterTabsIOS.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

require "json"

package = JSON.parse(File.read(File.join(__dir__, "../" "package.json")))

Pod::Spec.new do |s|
s.name = "RNTesterTabsIOS"
s.version = package["version"]
s.summary = package["description"]
s.description = "RNTester's native tab bar"
s.homepage = "https://github.com/react/react-native"
s.license = "MIT"
s.platforms = min_supported_versions
s.author = "Meta Platforms, Inc. and its affiliates"
s.source = { :git => "https://github.com/react/react-native.git", :tag => "#{s.version}" }
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"${PODS_CONFIGURATION_BUILD_DIR}/ReactCodegen/ReactCodegen.framework/Headers\"",
"CLANG_CXX_LANGUAGE_STANDARD" => rct_cxx_language_standard()
}

s.source_files = "ios/**/*.{h,mm}"
s.requires_arc = true

install_modules_dependencies(s)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import <React/RCTViewComponentView.h>

NS_ASSUME_NONNULL_BEGIN

/**
* Hosts RNTester's tabs in a UITabBarController, so the tab bar adapts to the
* device (a bottom bar, or a sidebar on wide layouts) while the app stays a
* single React Native root.
*
* The React children are the selected tab's content: they are mounted into a
* single container view, which moves into the selected tab's view controller.
*/
@interface RNTesterTabsIOSComponentView : RCTViewComponentView
@end

NS_ASSUME_NONNULL_END
228 changes: 228 additions & 0 deletions packages/rn-tester/RNTesterTabsIOS/ios/RNTesterTabsIOSComponentView.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#import "RNTesterTabsIOSComponentView.h"

#import <React/RCTConversions.h>
#import <React/UIView+React.h>
#import <react/renderer/components/AppSpecs/ComponentDescriptors.h>
#import <react/renderer/components/AppSpecs/EventEmitters.h>
#import <react/renderer/components/AppSpecs/Props.h>

#include <algorithm>

using namespace facebook::react;

/**
* A tab's view controller, which reports when the tab bar or window changes
* the area its content needs to clear.
*/
@interface RNTesterTabViewController : UIViewController
@property (nonatomic, copy) void (^safeAreaInsetsDidChange)(UIEdgeInsets insets);
@end

@implementation RNTesterTabViewController

- (void)viewSafeAreaInsetsDidChange
{
[super viewSafeAreaInsetsDidChange];
if (self.safeAreaInsetsDidChange != nil) {
self.safeAreaInsetsDidChange(self.view.safeAreaInsets);
}
}

@end

@interface RNTesterTabsIOSComponentView () <UITabBarControllerDelegate>
@end

@implementation RNTesterTabsIOSComponentView {
UITabBarController *_tabBarController;
// Holds the mounted React children, and moves into the selected tab.
UIView *_containerView;
std::vector<std::string> _tabKeys;
UIEdgeInsets _contentInsets;
}

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RNTesterTabsIOSComponentDescriptor>();
}

// The view owns a view controller, which is not worth resetting for reuse.
+ (BOOL)shouldBeRecycled
{
return NO;
}

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<const RNTesterTabsIOSProps>();
_props = defaultProps;

_containerView = [UIView new];
_containerView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;

_tabBarController = [UITabBarController new];
_tabBarController.delegate = self;
self.contentView = _tabBarController.view;
}
return self;
}

- (void)didMoveToWindow
{
[super didMoveToWindow];
if (self.window == nil) {
[_tabBarController willMoveToParentViewController:nil];
[_tabBarController removeFromParentViewController];
return;
}

UIViewController *parentViewController = self.reactViewController;
if (parentViewController != nil && _tabBarController.parentViewController == nil) {
[parentViewController addChildViewController:_tabBarController];
[_tabBarController didMoveToParentViewController:parentViewController];
}
}

#pragma mark - Children

// Children are laid out against this view's bounds, which the tab's view shares, so they keep their
// frames wherever the container moves.
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_containerView insertSubview:childComponentView atIndex:index];
}

- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
}

#pragma mark - Props

- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldTabsProps = static_cast<const RNTesterTabsIOSProps &>(*_props);
const auto &newTabsProps = static_cast<const RNTesterTabsIOSProps &>(*props);

// RNTester's tabs are a constant, so they are built once, from the first props.
bool tabsCreated = _tabKeys.empty();
if (tabsCreated) {
[self createTabs:newTabsProps.tabs];
}
if (tabsCreated || oldTabsProps.selectedTab != newTabsProps.selectedTab) {
[self selectTab:newTabsProps.selectedTab];
}
if (oldTabsProps.tabBarHidden != newTabsProps.tabBarHidden) {
if (@available(iOS 18.0, tvOS 18.0, *)) {
[_tabBarController setTabBarHidden:newTabsProps.tabBarHidden animated:NO];
} else {
_tabBarController.tabBar.hidden = newTabsProps.tabBarHidden;
}
}

[super updateProps:props oldProps:oldProps];
}

- (void)createTabs:(const std::vector<RNTesterTabsIOSTabsStruct> &)tabs
{
__weak RNTesterTabsIOSComponentView *weakSelf = self;
NSMutableArray<UIViewController *> *viewControllers = [NSMutableArray arrayWithCapacity:tabs.size()];
_tabKeys.clear();

for (const auto &tab : tabs) {
auto *viewController = [RNTesterTabViewController new];
viewController.tabBarItem =
[[UITabBarItem alloc] initWithTitle:RCTNSStringFromString(tab.title)
image:[UIImage systemImageNamed:RCTNSStringFromString(tab.systemImage)]
tag:0];
viewController.tabBarItem.accessibilityIdentifier = RCTNSStringFromString(tab.testID);
viewController.safeAreaInsetsDidChange = ^(UIEdgeInsets insets) {
[weakSelf tabSafeAreaInsetsDidChange:insets];
};
[viewControllers addObject:viewController];
_tabKeys.push_back(tab.key);
}

_tabBarController.viewControllers = viewControllers;
}

- (void)selectTab:(const std::string &)key
{
auto it = std::find(_tabKeys.begin(), _tabKeys.end(), key);
if (it == _tabKeys.end()) {
return;
}
_tabBarController.selectedIndex = std::distance(_tabKeys.begin(), it);

UIView *tabView = _tabBarController.selectedViewController.view;
_containerView.frame = tabView.bounds;
[tabView addSubview:_containerView];
}

#pragma mark - UITabBarControllerDelegate

- (BOOL)tabBarController:(UITabBarController *)tabBarController
shouldSelectViewController:(UIViewController *)viewController
{
NSUInteger index = [tabBarController.viewControllers indexOfObject:viewController];
if (index != NSNotFound && _eventEmitter != nullptr) {
static_cast<const RNTesterTabsIOSEventEmitter &>(*_eventEmitter).onTabPress({.key = _tabKeys[index]});
}
// JS selects the tab by updating `selectedTab`.
return NO;
}

#pragma mark - Insets

- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
// The insets can settle before the first event emitter arrives.
UIViewController *selectedViewController = _tabBarController.selectedViewController;
if (selectedViewController.isViewLoaded && selectedViewController.view.window != nil) {
_contentInsets = UIEdgeInsetsZero;
[self tabSafeAreaInsetsDidChange:selectedViewController.view.safeAreaInsets];
}
}

- (void)tabSafeAreaInsetsDidChange:(UIEdgeInsets)insets
{
// The home indicator's inset only applies beneath a tab bar along the bottom edge, which already
// clears it. With the tab bar at the side, content runs to the bottom edge.
CGRect tabBarFrame = _tabBarController.tabBar.frame;
CGRect bounds = _tabBarController.view.bounds;
if (CGRectGetWidth(tabBarFrame) < CGRectGetWidth(bounds) || CGRectGetMaxY(tabBarFrame) < CGRectGetMaxY(bounds)) {
insets.bottom = 0;
}
// With no status bar above it, UINavigationBar still sits 24pt below the top edge, clear of the
// display's corners. Content leaves the same gap, since the device reports no top inset there.
if (insets.top == 0) {
insets.top = 24;
}
if (UIEdgeInsetsEqualToEdgeInsets(insets, _contentInsets) || _eventEmitter == nullptr) {
return;
}
_contentInsets = insets;
static_cast<const RNTesterTabsIOSEventEmitter &>(*_eventEmitter)
.onContentInsetsChange({
.top = insets.top,
.left = insets.left,
.bottom = insets.bottom,
.right = insets.right,
});
}

@end

Class<RCTComponentViewProtocol> RNTesterTabsIOSCls(void)
{
return RNTesterTabsIOSComponentView.class;
}
Loading
Loading