first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
#pragma once
#import "RNSDefines.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <UIKit/UIKit.h>
#import "RNSBottomTabsAccessoryComponentView.h"
#import "RNSBottomTabsAccessoryContentComponentView.h"
#import "RNSEnums.h"
NS_ASSUME_NONNULL_BEGIN
/**
* @class RNSBottomAccessoryHelper
* @brief Class responsible for managing accessory size and environment changes for
* RNSBottomTabsAccessoryComponentView.
*/
API_AVAILABLE(ios(26.0))
@interface RNSBottomAccessoryHelper : NSObject
- (instancetype)initWithBottomAccessoryView:(RNSBottomTabsAccessoryComponentView *)bottomAccessoryView;
/**
* Registers KVO for frames of UIKit's bottom accessory wrapper view.
* It must be called after `RNSBottomTabsAccessoryComponentView` or its ancestor is set as `bottomAccessory` on
* `RNSTabBarController`.
*/
- (void)registerForAccessoryFrameChanges;
/**
* Invalidates observers, display link (if it is used); resets internal properties.
*/
- (void)invalidate;
@end
#pragma mark - Content view switching workaround
#if defined(__cplusplus) && REACT_NATIVE_VERSION_MINOR >= 82
/**
* Due to *synchronous* events not being actually *synchronous*, we are unable to handle layout modifications
* in reaction to environment change (e.g. subviews being mounted/unmounted; changes to size/origin are synchronous
* thanks to synchronous state updates and work correctly).
* In order to mitigate this, we introduced a workaround approach: 2 views are rendered all the time on top of each
* other. One is for `regular` environment and the second one is for `inline` environment. When environment changes, we
* swap which view is actually visible by changing opacity.
*/
@interface RNSBottomAccessoryHelper ()
/**
* Allows to set which `RNSBottomTabsAccessoryContentComponentView` instance should be visible
* for which accessory environment.
*/
- (void)setContentView:(nullable RNSBottomTabsAccessoryContentComponentView *)contentView
forEnvironment:(RNSBottomTabsAccessoryEnvironment)environment;
/**
* If `contentView` is set for both environments, sets opacity according to current tab accessory `environent`.
* Otherwise, it is a no-op.
*/
- (void)handleContentViewVisibilityForEnvironmentIfNeeded;
@end
#endif // defined(__cplusplus) && REACT_NATIVE_VERSION_MINOR >= 82
NS_ASSUME_NONNULL_END
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,209 @@
#import "RNSBottomAccessoryHelper.h"
#import "RNSBottomTabsAccessoryShadowStateProxy.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <React/RCTAssert.h>
#import <cxxreact/ReactNativeVersion.h>
namespace react = facebook::react;
@implementation RNSBottomAccessoryHelper {
RNSBottomTabsAccessoryComponentView *__weak _bottomAccessoryView;
#if REACT_NATIVE_VERSION_MINOR < 82
BOOL _initialStateUpdateSent;
CADisplayLink *_displayLink;
#else // REACT_NATIVE_VERSION_MINOR < 82
RNSBottomTabsAccessoryContentComponentView *__weak _regularContentView;
RNSBottomTabsAccessoryContentComponentView *__weak _inlineContentView;
#endif // REACT_NATIVE_VERSION_MINOR < 82
id<UITraitChangeRegistration> _traitChangeRegistration;
}
- (instancetype)initWithBottomAccessoryView:(RNSBottomTabsAccessoryComponentView *)bottomAccessoryView
{
if (self = [super init]) {
_bottomAccessoryView = bottomAccessoryView;
[self initState];
_traitChangeRegistration = [self registerForAccessoryEnvironmentChanges];
}
return self;
}
- (void)initState
{
#if REACT_NATIVE_VERSION_MINOR < 82
_initialStateUpdateSent = NO;
_displayLink = nil;
#else // REACT_NATIVE_VERSION_MINOR < 82
_regularContentView = nil;
_inlineContentView = nil;
#endif // REACT_NATIVE_VERSION_MINOR < 82
}
#pragma mark - Content view switching workaround
#if REACT_NATIVE_VERSION_MINOR >= 82
- (BOOL)isContentViewSwitchingWorkaroundActive
{
return _regularContentView != nil && _inlineContentView != nil;
}
- (void)setContentView:(RNSBottomTabsAccessoryContentComponentView *)contentView
forEnvironment:(RNSBottomTabsAccessoryEnvironment)environment
{
switch (environment) {
case RNSBottomTabsAccessoryEnvironmentRegular:
_regularContentView = contentView;
break;
case RNSBottomTabsAccessoryEnvironmentInline:
_inlineContentView = contentView;
break;
default:
RCTLogError(@"[RNScreens] Unsupported BottomTabsAccessory environment");
}
[self handleContentViewVisibilityForEnvironmentIfNeeded];
}
- (void)handleContentViewVisibilityForEnvironmentIfNeeded
{
if (!self.isContentViewSwitchingWorkaroundActive) {
return;
}
switch (self->_bottomAccessoryView.traitCollection.tabAccessoryEnvironment) {
case UITabAccessoryEnvironmentInline:
_regularContentView.layer.opacity = 0.0;
_inlineContentView.layer.opacity = 1.0;
break;
default:
_regularContentView.layer.opacity = 1.0;
_inlineContentView.layer.opacity = 0.0;
break;
}
}
#endif // REACT_NATIVE_VERSION_MINOR >= 82
#pragma mark - Observing environment changes
- (id<UITraitChangeRegistration>)registerForAccessoryEnvironmentChanges
{
return [_bottomAccessoryView
registerForTraitChanges:@[ [UITraitTabAccessoryEnvironment class] ]
withHandler:^(__kindof id<UITraitEnvironment>, UITraitCollection *previousTraitCollection) {
UITabAccessoryEnvironment environment =
self->_bottomAccessoryView.traitCollection.tabAccessoryEnvironment;
[self->_bottomAccessoryView.reactEventEmitter emitOnEnvironmentChangeIfNeeded:environment];
#if REACT_NATIVE_VERSION_MINOR >= 82
[self handleContentViewVisibilityForEnvironmentIfNeeded];
#endif // REACT_NATIVE_VERSION_MINOR >= 82
}];
}
#pragma mark - Observing frame changes
- (void)registerForAccessoryFrameChanges
{
[self.nativeWrapperView addObserver:self forKeyPath:@"center" options:NSKeyValueObservingOptionInitial context:nil];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
[self notifyWrapperViewFrameHasChanged];
}
- (UIView *)nativeWrapperView
{
RCTAssert(
_bottomAccessoryView.superview.superview != nil,
@"[RNScreens] RNSBottomTabsAccessoryComponentView must be the set as bottom accessory.");
return _bottomAccessoryView.superview.superview;
}
- (void)notifyWrapperViewFrameHasChanged
{
#if REACT_NATIVE_VERSION_MINOR < 82
// Make sure that bottom accessory's size is sent to ShadowNode as soon as possible.
// We set origin to (0,0) because initially self.nativeWrapperView's origin is incorrect.
// We want the enable the display link as well so that it takes over later with correct origin.
if (!_initialStateUpdateSent) {
CGRect frame = CGRectMake(0, 0, self.nativeWrapperView.frame.size.width, self.nativeWrapperView.frame.size.height);
[_bottomAccessoryView.shadowStateProxy updateShadowStateWithFrame:frame];
_initialStateUpdateSent = YES;
}
if (_displayLink == nil) {
[self setupDisplayLink];
}
#else // REACT_NATIVE_VERSION_MINOR < 82
// We use self.nativeWrapperView because it has both the size and the origin
// that we want to send to the ShadowNode.
[_bottomAccessoryView.shadowStateProxy updateShadowStateWithFrame:self.nativeWrapperView.frame];
#endif // REACT_NATIVE_VERSION_MINOR < 82
}
#if REACT_NATIVE_VERSION_MINOR < 82
- (void)setupDisplayLink
{
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)handleDisplayLink:(CADisplayLink *)sender
{
// We use self.nativeWrapperView because it has both the size and the origin
// that we want to send to the ShadowNode.
CGRect presentationFrame = self.nativeWrapperView.layer.presentationLayer.frame;
if (CGRectEqualToRect(presentationFrame, CGRectZero)) {
return;
}
[_bottomAccessoryView.shadowStateProxy updateShadowStateWithFrame:presentationFrame];
// self.nativeWrapperView.frame is set to final value at the beginning of the transition.
// When frame from presentation layer matches self.nativeWrapperView.frame, it indicates that
// the transition is over and we can disable the display link.
if (CGRectEqualToRect(presentationFrame, self.nativeWrapperView.frame)) {
[self invalidateDisplayLink];
}
}
- (void)invalidateDisplayLink
{
[_displayLink invalidate];
_displayLink = nil;
}
#endif // REACT_NATIVE_VERSION_MINOR < 82
#pragma mark - Invalidation
- (void)invalidate
{
[_bottomAccessoryView unregisterForTraitChanges:_traitChangeRegistration];
_traitChangeRegistration = nil;
// Using nativeWrapperView directly here to avoid failing RCTAssert in self.nativeWrapperView.
// If we're called from didMoveToWindow, it's not a problem, but I'm not sure if this will always be the case.
[_bottomAccessoryView.superview.superview removeObserver:self forKeyPath:@"center"];
_bottomAccessoryView = nil;
#if REACT_NATIVE_VERSION_MINOR < 82
[self invalidateDisplayLink];
#else // REACT_NATIVE_VERSION_MINOR < 82
_regularContentView = nil;
_inlineContentView = nil;
#endif // REACT_NATIVE_VERSION_MINOR < 82
}
@end
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,97 @@
#pragma once
#import "RNSBottomTabsAccessoryEventEmitter.h"
#import "RNSBottomTabsHostComponentView.h"
#import "RNSReactBaseView.h"
#if RCT_NEW_ARCH_ENABLED
#import "RNSViewControllerInvalidating.h"
#else
#import <React/RCTBridge.h>
#import <React/RCTInvalidating.h>
#endif
#if RCT_NEW_ARCH_ENABLED && defined(__cplusplus)
#import <rnscreens/RNSBottomTabsAccessoryComponentDescriptor.h>
#endif // RCT_NEW_ARCH_ENABLED && defined(__cplusplus)
NS_ASSUME_NONNULL_BEGIN
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
@class RNSBottomAccessoryHelper;
@class RNSBottomTabsAccessoryShadowStateProxy;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
@interface RNSBottomTabsAccessoryComponentView : RNSReactBaseView <
#if RCT_NEW_ARCH_ENABLED
RNSViewControllerInvalidating
#else // RCT_NEW_ARCH_ENABLED
RCTInvalidating
#endif // RCT_NEW_ARCH_ENABLED
>
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge;
@property (nonatomic, weak, readonly, nullable) RCTBridge *bridge;
#endif // !RCT_NEW_ARCH_ENABLED
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
/**
* If not null, the bottom accesory's helper that handles accessory size and environment changes.
* It also manages *content view switching workaround* for RN >= 0.82.
*/
@property (nonatomic, strong, readonly, nullable) RNSBottomAccessoryHelper *helper;
/**
* If not null, the bottom accesory's shadow state proxy that handles communication with ShadowTree.
*/
@property (nonatomic, strong, readonly, nullable) RNSBottomTabsAccessoryShadowStateProxy *shadowStateProxy;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
/**
* If not null, the bottom tabs host view that this accessory component view belongs to.
*/
@property (nonatomic, weak, nullable) RNSBottomTabsHostComponentView *bottomTabsHostView;
@end
#pragma mark - React Events
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
@interface RNSBottomTabsAccessoryComponentView ()
/**
* Use returned object to emit appropriate React Events to Element Tree.
*/
- (nonnull RNSBottomTabsAccessoryEventEmitter *)reactEventEmitter;
#if !RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY Event blocks
@property (nonatomic, copy) RCTDirectEventBlock onEnvironmentChange;
#endif // !RCT_NEW_ARCH_ENABLED
@end
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#pragma mark - Hidden from Swift
#if RCT_NEW_ARCH_ENABLED && defined(__cplusplus)
@interface RNSBottomTabsAccessoryComponentView ()
- (facebook::react::RNSBottomTabsAccessoryShadowNode::ConcreteState::Shared)state;
@end
#endif // RCT_NEW_ARCH_ENABLED && defined(__cplusplus)
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,199 @@
#import "RNSBottomTabsAccessoryComponentView.h"
#import "RNSBottomAccessoryHelper.h"
#import "RNSBottomTabsAccessoryShadowStateProxy.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
#import <rnscreens/RNSBottomTabsAccessoryComponentDescriptor.h>
#endif // RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#pragma mark - View implementation
@implementation RNSBottomTabsAccessoryComponentView {
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
RNSBottomAccessoryHelper *_helper API_AVAILABLE(ios(26.0));
RNSBottomTabsAccessoryShadowStateProxy *_shadowStateProxy API_AVAILABLE(ios(26.0));
RNSBottomTabsAccessoryEventEmitter *_Nonnull _reactEventEmitter;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
RNSBottomTabsHostComponentView *__weak _Nullable _bottomTabsHostView;
#if RCT_NEW_ARCH_ENABLED
react::RNSBottomTabsAccessoryShadowNode::ConcreteState::Shared _state;
#else // RCT_NEW_ARCH_ENABLED
__weak RCTBridge *_bridge;
#endif // RCT_NEW_ARCH_ENABLED
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
#if !RCT_NEW_ARCH_ENABLED
- (instancetype)initWithFrame:(CGRect)frame bridge:(RCTBridge *)bridge
{
if (self = [self initWithFrame:frame]) {
_bridge = bridge;
}
return self;
}
- (RCTBridge *)bridge
{
return _bridge;
}
#endif // !RCT_NEW_ARCH_ENABLED
- (void)initState
{
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
if (@available(iOS 26, *)) {
_helper = [[RNSBottomAccessoryHelper alloc] initWithBottomAccessoryView:self];
_shadowStateProxy = [[RNSBottomTabsAccessoryShadowStateProxy alloc] initWithBottomAccessoryView:self];
_reactEventEmitter = [RNSBottomTabsAccessoryEventEmitter new];
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
_bottomTabsHostView = nil;
}
#pragma mark - UIKit callbacks
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
- (void)didMoveToWindow
{
if (self.window != nil) {
[_helper registerForAccessoryFrameChanges];
} else {
#if RCT_NEW_ARCH_ENABLED
[self invalidateController];
#else // RCT_NEW_ARCH_ENABLED
[self invalidate];
#endif // RCT_NEW_ARCH_ENABLED
}
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTViewComponentViewProtocol
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
- (void)updateState:(const react::State::Shared &)state oldState:(const react::State::Shared &)oldState
{
[super updateState:state oldState:oldState];
_state = std::static_pointer_cast<const react::RNSBottomTabsAccessoryComponentDescriptor::ConcreteState>(state);
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
const auto &castedEventEmitter =
std::static_pointer_cast<const react::RNSBottomTabsAccessoryEventEmitter>(eventEmitter);
[_reactEventEmitter updateEventEmitter:castedEventEmitter];
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsAccessoryComponentDescriptor>();
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
// We could consider enabling it someday though.
return NO;
}
- (facebook::react::RNSBottomTabsAccessoryShadowNode::ConcreteState::Shared)state
{
return _state;
}
#pragma mark - RNSViewControllerInvalidating
- (void)invalidateController
{
[self invalidateImpl];
}
- (BOOL)shouldInvalidateOnMutation:(const facebook::react::ShadowViewMutation &)mutation
{
// For bottom tabs, Host is responsible for invalidating children.
return NO;
}
#else // RCT_NEW_ARCH_ENABLED
#pragma mark - LEGACY architecture implementation
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
- (void)setOnEnvironmentChange:(RCTDirectEventBlock)onEnvironmentChange
{
[self.reactEventEmitter setOnEnvironmentChange:onEnvironmentChange];
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#pragma mark - RCTInvalidating
- (void)invalidate
{
[self invalidateImpl];
}
#endif // RCT_NEW_ARCH_ENABLED
- (void)invalidateImpl
{
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
[_helper invalidate];
_helper = nil;
[_shadowStateProxy invalidate];
_shadowStateProxy = nil;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
_state.reset();
#endif // RCT_NEW_ARCH_ENABLED
}
#pragma mark - React events
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
- (nonnull RNSBottomTabsAccessoryEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsAccessoryCls(void)
{
return RNSBottomTabsAccessoryComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,15 @@
#pragma once
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsAccessoryComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END
#endif // !RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,25 @@
#import "RNSBottomTabsAccessoryComponentViewManager.h"
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTImageLoader.h>
#import "RNSBottomTabsAccessoryComponentView.h"
@implementation RNSBottomTabsAccessoryComponentViewManager
// TODO: This is legacy arch only - remove when no longer needed
RCT_EXPORT_MODULE(RNSBottomTabsAccessoryManager)
- (UIView *)view
{
// For Paper, we need to initialize TabsAccessory with bridge
return [[RNSBottomTabsAccessoryComponentView alloc] initWithFrame:CGRectZero bridge:self.bridge];
}
#pragma mark - LEGACY Events
RCT_EXPORT_VIEW_PROPERTY(onEnvironmentChange, RCTDirectEventBlock);
@end
#endif // !RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,24 @@
#pragma once
#import "RNSDefines.h"
#import "RNSEnums.h"
#import "RNSReactBaseView.h"
#if defined(__cplusplus)
#import <cxxreact/ReactNativeVersion.h>
#endif // defined(__cplusplus)
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsAccessoryContentComponentView : RNSReactBaseView
#if RNS_BOTTOM_ACCESSORY_AVAILABLE && defined(__cplusplus) && REACT_NATIVE_VERSION_MINOR >= 82
@property (nonatomic) RNSBottomTabsAccessoryEnvironment environment;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE && defined(__cplusplus) && REACT_NATIVE_VERSION_MINOR >= 82
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,122 @@
#import "RNSBottomTabsAccessoryContentComponentView.h"
#import "RNSBottomAccessoryHelper.h"
#import "RNSBottomTabsAccessoryComponentView.h"
#import "RNSConversions.h"
#if RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#endif // RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#pragma mark - View implementation
@implementation RNSBottomTabsAccessoryContentComponentView {
#if RNS_BOTTOM_ACCESSORY_AVAILABLE && REACT_NATIVE_VERSION_MINOR >= 82
RNSBottomTabsAccessoryComponentView *__weak _Nullable _accessoryView;
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE && REACT_NATIVE_VERSION_MINOR >= 82
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
return self;
}
#pragma mark - UIKit callbacks
#if RNS_BOTTOM_ACCESSORY_AVAILABLE && REACT_NATIVE_VERSION_MINOR >= 82
- (void)didMoveToWindow
{
if ([self.superview isKindOfClass:[RNSBottomTabsAccessoryComponentView class]]) {
RNSBottomTabsAccessoryComponentView *accessoryView =
static_cast<RNSBottomTabsAccessoryComponentView *>(self.superview);
_accessoryView = accessoryView;
[_accessoryView.helper setContentView:(self.window != nil ? self : nil) forEnvironment:_environment];
} else {
[_accessoryView.helper setContentView:nil forEnvironment:_environment];
_accessoryView = nil;
}
}
// `RCTViewComponentView` uses this deprecated callback to invalidate layer when trait collection
// `hasDifferentColorAppearanceComparedToTraitCollection`. This updates opacity which breaks our
// content view switching workaround. To mitigate this, we update content view visibility after
// RCTViewComponentView handles the change. We need to use the same deprecated callback as it's
// called after callbacks registered via the new API.
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
[_accessoryView.helper handleContentViewVisibilityForEnvironmentIfNeeded];
}
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE && REACT_NATIVE_VERSION_MINOR >= 82
#if RCT_NEW_ARCH_ENABLED
#pragma mark - RCTViewComponentViewProtocol
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#if REACT_NATIVE_VERSION_MINOR >= 82
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsAccessoryContentProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSBottomTabsAccessoryContentProps>(props);
if (newComponentProps.environment != oldComponentProps.environment) {
_environment =
rnscreens::conversion::RNSBottomTabsAccessoryEnvironmentFromCppEquivalent(newComponentProps.environment);
}
[super updateProps:props oldProps:oldProps];
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
// In finalize updates, `invalidateLayer` is called. It resets `view.layer.opacity`
// which we use to switch visible bottom accessory content view. In order to mitigate
// this, we update visibility after `[super finalizeUpdates:updateMask]`. Without this,
// both content views are visible on first render. It does not happen on subsequent
// renders because `updateState` is called before trait changes but there might be other
// cases when `finalizeUpdates` will run so to make sure that we maintain correct
// visibility, we call `handleContentViewVisibilityForEnvironmentIfNeeded` here.
[_accessoryView.helper handleContentViewVisibilityForEnvironmentIfNeeded];
}
#endif // REACT_NATIVE_VERSION_MINOR >= 82
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSBottomTabsAccessoryContentComponentDescriptor>();
}
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE
+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
// We could consider enabling it someday though.
return NO;
}
#endif // RCT_NEW_ARCH_ENABLED
@end
#if RCT_NEW_ARCH_ENABLED
#pragma mark - View class exposure
Class<RCTComponentViewProtocol> RNSBottomTabsAccessoryContentCls(void)
{
return RNSBottomTabsAccessoryContentComponentView.class;
}
#endif // RCT_NEW_ARCH_ENABLED

View File

@@ -0,0 +1,52 @@
#pragma once
#import "RNSDefines.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // defined(__cplusplus) && RCT_NEW_ARCH_ENABLED
#if !RCT_NEW_ARCH_ENABLED
#import <React/RCTComponent.h>
#endif // !RCT_NEW_ARCH_ENABLED
NS_ASSUME_NONNULL_BEGIN
@interface RNSBottomTabsAccessoryEventEmitter : NSObject
- (BOOL)emitOnEnvironmentChangeIfNeeded:(UITabAccessoryEnvironment)environment API_AVAILABLE(ios(26.0));
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSBottomTabsAccessoryEventEmitter ()
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsAccessoryEventEmitter> &)emitter;
#else
#pragma mark - LEGACY Event emitter blocks
@property (nonatomic, copy) RCTDirectEventBlock onEnvironmentChange;
#endif // RCT_NEW_ARCH_ENABLED
@end
#endif // defined(__cplusplus)
NS_ASSUME_NONNULL_END
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,80 @@
#import "RNSBottomTabsAccessoryEventEmitter.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#import <React/RCTLog.h>
#import "RNSConversions.h"
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <react/renderer/components/rnscreens/EventEmitters.h>
#endif // RCT_NEW_ARCH_ENABLED
#if RCT_NEW_ARCH_ENABLED
namespace react = facebook::react;
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSBottomTabsAccessoryEventEmitter {
#if RCT_NEW_ARCH_ENABLED
std::shared_ptr<const react::RNSBottomTabsAccessoryEventEmitter> _reactEventEmitter;
#endif // RCT_NEW_ARCH_ENABLED
}
- (instancetype)init
{
if (self = [super init]) {
#if RCT_NEW_ARCH_ENABLED
_reactEventEmitter = nullptr;
#endif // RCT_NEW_ARCH_ENABLED
}
return self;
}
#if RCT_NEW_ARCH_ENABLED
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSBottomTabsAccessoryEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}
#endif // RCT_NEW_ARCH_ENABLED
- (BOOL)emitOnEnvironmentChangeIfNeeded:(UITabAccessoryEnvironment)environment API_AVAILABLE(ios(26.0))
{
#if RCT_NEW_ARCH_ENABLED
if (_reactEventEmitter != nullptr) {
auto payloadEnvironment =
rnscreens::conversion::RNSBottomTabsAccessoryOnEnvironmentChangePayloadFromUITabAccessoryEnvironment(
environment);
// If environment is other than `regular` or `inline`, we don't emit the event.
if (!payloadEnvironment.has_value()) {
return NO;
}
_reactEventEmitter->onEnvironmentChange({.environment = payloadEnvironment.value()});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnEnvironmentChange event emission due to nullish emitter");
return NO;
}
#else
if (self.onEnvironmentChange) {
NSString *environmentString =
rnscreens::conversion::RNSBottomTabsAccessoryOnEnvironmentChangePayloadFromUITabAccessoryEnvironment(
environment);
// If environment is other than `regular` or `inline`, we don't emit the event.
if (environmentString == nil) {
return NO;
}
self.onEnvironmentChange(@{@"environment" : environmentString});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnEnvironmentChange event emission due to nullish emitter");
return NO;
}
#endif // RCT_NEW_ARCH_ENABLED
}
@end
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,35 @@
#pragma once
#import "RNSBottomAccessoryHelper.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
NS_ASSUME_NONNULL_BEGIN
@class RNSBottomTabsAccessoryComponentView;
/**
* @class RNSBottomTabsAccessoryShadowStateProxy
* @brief Class responsible for communication with ShadowTree for
* RNSBottomTabsAccessoryComponentView.
*/
API_AVAILABLE(ios(26.0))
@interface RNSBottomTabsAccessoryShadowStateProxy : NSObject
- (instancetype)initWithBottomAccessoryView:(RNSBottomTabsAccessoryComponentView *)bottomAccessoryView;
/**
* Updates bottom accessory's frame in ShadowTree if new frame is different than previously sent frame.
*/
- (void)updateShadowStateWithFrame:(CGRect)frame;
/**
* Resets internal properties.
*/
- (void)invalidate;
@end
NS_ASSUME_NONNULL_END
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE

View File

@@ -0,0 +1,62 @@
#import "RNSBottomTabsAccessoryShadowStateProxy.h"
#if RNS_BOTTOM_ACCESSORY_AVAILABLE
#if RCT_NEW_ARCH_ENABLED
#import <React/RCTConversions.h>
#import <rnscreens/RNSBottomTabsAccessoryShadowNode.h>
#else // RCT_NEW_ARCH_ENABLED
#import <React/RCTUIManager.h>
#endif // RCT_NEW_ARCH_ENABLED
@implementation RNSBottomTabsAccessoryShadowStateProxy {
RNSBottomTabsAccessoryComponentView *__weak _bottomAccessoryView;
CGRect _previousFrame;
}
- (instancetype)initWithBottomAccessoryView:(RNSBottomTabsAccessoryComponentView *)bottomAccessoryView
{
if (self = [super init]) {
_bottomAccessoryView = bottomAccessoryView;
[self initState];
}
return self;
}
- (void)initState
{
_previousFrame = CGRectZero;
}
- (void)updateShadowStateWithFrame:(CGRect)frame
{
if (!CGRectEqualToRect(frame, _previousFrame)) {
#if RCT_NEW_ARCH_ENABLED
if (_bottomAccessoryView.state != nullptr) {
auto newState =
react::RNSBottomTabsAccessoryState{RCTSizeFromCGSize(frame.size), RCTPointFromCGPoint(frame.origin)};
_bottomAccessoryView.state->updateState(
std::move(newState)
#if REACT_NATIVE_VERSION_MINOR >= 82
,
facebook::react::EventQueue::UpdateMode::unstable_Immediate
#endif // REACT_NATIVE_VERSION_MINOR >= 82
);
_previousFrame = frame;
}
#else // RCT_NEW_ARCH_ENABLED
[_bottomAccessoryView.bridge.uiManager setSize:frame.size forView:_bottomAccessoryView];
_previousFrame = frame;
#endif // RCT_NEW_ARCH_ENABLED
}
}
- (void)invalidate
{
_previousFrame = CGRectZero;
}
@end
#endif // RNS_BOTTOM_ACCESSORY_AVAILABLE