feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,21 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/**
* UIView class for root <ActivityIndicator> component.
*/
@interface RCTActivityIndicatorViewComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,99 @@
/*
* 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 "RCTActivityIndicatorViewComponentView.h"
#import <React/RCTConversions.h>
#import <react/renderer/components/rncore/ComponentDescriptors.h>
#import <react/renderer/components/rncore/EventEmitters.h>
#import <react/renderer/components/rncore/Props.h>
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
static UIActivityIndicatorViewStyle convertActivityIndicatorViewStyle(const ActivityIndicatorViewSize &size)
{
switch (size) {
case ActivityIndicatorViewSize::Small:
return UIActivityIndicatorViewStyleMedium;
case ActivityIndicatorViewSize::Large:
return UIActivityIndicatorViewStyleLarge;
}
}
@implementation RCTActivityIndicatorViewComponentView {
UIActivityIndicatorView *_activityIndicatorView;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<ActivityIndicatorViewComponentDescriptor>();
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
const auto &defaultProps = ActivityIndicatorViewShadowNode::defaultSharedProps();
_props = defaultProps;
_activityIndicatorView = [[UIActivityIndicatorView alloc] initWithFrame:self.bounds];
_activityIndicatorView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (defaultProps->animating) {
[_activityIndicatorView startAnimating];
} else {
[_activityIndicatorView stopAnimating];
}
_activityIndicatorView.color = RCTUIColorFromSharedColor(defaultProps->color);
_activityIndicatorView.hidesWhenStopped = defaultProps->hidesWhenStopped;
_activityIndicatorView.activityIndicatorViewStyle = convertActivityIndicatorViewStyle(defaultProps->size);
[self addSubview:_activityIndicatorView];
}
return self;
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldViewProps = static_cast<const ActivityIndicatorViewProps &>(*_props);
const auto &newViewProps = static_cast<const ActivityIndicatorViewProps &>(*props);
if (oldViewProps.animating != newViewProps.animating) {
if (newViewProps.animating) {
[_activityIndicatorView startAnimating];
} else {
[_activityIndicatorView stopAnimating];
}
}
if (oldViewProps.color != newViewProps.color) {
_activityIndicatorView.color = RCTUIColorFromSharedColor(newViewProps.color);
}
// TODO: This prop should be deprecated.
if (oldViewProps.hidesWhenStopped != newViewProps.hidesWhenStopped) {
_activityIndicatorView.hidesWhenStopped = newViewProps.hidesWhenStopped;
}
if (oldViewProps.size != newViewProps.size) {
_activityIndicatorView.activityIndicatorViewStyle = convertActivityIndicatorViewStyle(newViewProps.size);
}
[super updateProps:props oldProps:oldProps];
}
@end
Class<RCTComponentViewProtocol> RCTActivityIndicatorViewCls(void)
{
return RCTActivityIndicatorViewComponentView.class;
}

View File

@@ -0,0 +1,16 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
#import <react/renderer/components/rncore/RCTComponentViewHelpers.h>
@interface RCTDebuggingOverlayComponentView : RCTViewComponentView <RCTDebuggingOverlayViewProtocol>
@end

View File

@@ -0,0 +1,73 @@
/*
* 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 "RCTDebuggingOverlayComponentView.h"
#import <React/RCTDebuggingOverlay.h>
#import <React/RCTDefines.h>
#import <React/RCTLog.h>
#import <react/renderer/components/rncore/ComponentDescriptors.h>
#import <react/renderer/components/rncore/EventEmitters.h>
#import <react/renderer/components/rncore/Props.h>
#import <react/renderer/components/rncore/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@implementation RCTDebuggingOverlayComponentView {
RCTDebuggingOverlay *_overlay;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = DebuggingOverlayShadowNode::defaultSharedProps();
_overlay = [[RCTDebuggingOverlay alloc] initWithFrame:self.bounds];
self.contentView = _overlay;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<DebuggingOverlayComponentDescriptor>();
}
#pragma mark - Native commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTDebuggingOverlayHandleCommand(self, commandName, args);
}
- (void)highlightTraceUpdates:(NSArray *)updates
{
[_overlay highlightTraceUpdates:updates];
}
- (void)highlightElements:(NSArray *)elements
{
[_overlay highlightElements:elements];
}
- (void)clearElementsHighlights
{
[_overlay clearElementsHighlights];
}
@end
Class<RCTComponentViewProtocol> RCTDebuggingOverlayCls(void)
{
return RCTDebuggingOverlayComponentView.class;
}

View File

@@ -0,0 +1,24 @@
/*
* 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/RCTImageResponseDelegate.h>
#import <React/RCTUIImageViewAnimated.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/**
* UIView class for root <Image> component.
*/
@interface RCTImageComponentView : RCTViewComponentView <RCTImageResponseDelegate> {
@protected
RCTUIImageViewAnimated *_imageView;
}
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,208 @@
/*
* 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 "RCTImageComponentView.h"
#import <React/RCTAssert.h>
#import <React/RCTConversions.h>
#import <React/RCTImageBlurUtils.h>
#import <React/RCTImageResponseObserverProxy.h>
#import <react/renderer/components/image/ImageComponentDescriptor.h>
#import <react/renderer/components/image/ImageEventEmitter.h>
#import <react/renderer/components/image/ImageProps.h>
#import <react/renderer/imagemanager/ImageRequest.h>
#import <react/renderer/imagemanager/RCTImagePrimitivesConversions.h>
#import <react/utils/CoreFeatures.h>
using namespace facebook::react;
@implementation RCTImageComponentView {
ImageShadowNode::ConcreteState::Shared _state;
RCTImageResponseObserverProxy _imageResponseObserverProxy;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
const auto &defaultProps = ImageShadowNode::defaultSharedProps();
_props = defaultProps;
_imageView = [RCTUIImageViewAnimated new];
_imageView.clipsToBounds = YES;
_imageView.contentMode = RCTContentModeFromImageResizeMode(defaultProps->resizeMode);
_imageView.layer.minificationFilter = kCAFilterTrilinear;
_imageView.layer.magnificationFilter = kCAFilterTrilinear;
_imageResponseObserverProxy = RCTImageResponseObserverProxy(self);
self.contentView = _imageView;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<ImageComponentDescriptor>();
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldImageProps = static_cast<const ImageProps &>(*_props);
const auto &newImageProps = static_cast<const ImageProps &>(*props);
// `resizeMode`
if (oldImageProps.resizeMode != newImageProps.resizeMode) {
_imageView.contentMode = RCTContentModeFromImageResizeMode(newImageProps.resizeMode);
}
// `tintColor`
if (oldImageProps.tintColor != newImageProps.tintColor) {
_imageView.tintColor = RCTUIColorFromSharedColor(newImageProps.tintColor);
}
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
{
RCTAssert(state, @"`state` must not be null.");
RCTAssert(
std::dynamic_pointer_cast<ImageShadowNode::ConcreteState const>(state),
@"`state` must be a pointer to `ImageShadowNode::ConcreteState`.");
auto oldImageState = std::static_pointer_cast<ImageShadowNode::ConcreteState const>(_state);
auto newImageState = std::static_pointer_cast<ImageShadowNode::ConcreteState const>(state);
[self _setStateAndResubscribeImageResponseObserver:newImageState];
bool havePreviousData = oldImageState && oldImageState->getData().getImageSource() != ImageSource{};
if (!havePreviousData ||
(newImageState && newImageState->getData().getImageSource() != oldImageState->getData().getImageSource())) {
// Loading actually starts a little before this, but this is the first time we know
// the image is loading and can fire an event from this component
static_cast<const ImageEventEmitter &>(*_eventEmitter).onLoadStart();
// TODO (T58941612): Tracking for visibility should be done directly on this class.
// For now, we consolidate instrumentation logic in the image loader, so that pre-Fabric gets the same treatment.
}
}
- (void)_setStateAndResubscribeImageResponseObserver:(const ImageShadowNode::ConcreteState::Shared &)state
{
if (_state) {
const auto &imageRequest = _state->getData().getImageRequest();
auto &observerCoordinator = imageRequest.getObserverCoordinator();
observerCoordinator.removeObserver(_imageResponseObserverProxy);
// Cancelling image request because we are no longer observing it.
// This is not 100% correct place to do this because we may want to
// re-create RCTImageComponentView with the same image and if it
// was cancelled before downloaded, download is not resumed.
// This will only become issue if we decouple life cycle of a
// ShadowNode from ComponentView, which is not something we do now.
imageRequest.cancel();
}
_state = state;
if (_state) {
auto &observerCoordinator = _state->getData().getImageRequest().getObserverCoordinator();
observerCoordinator.addObserver(_imageResponseObserverProxy);
}
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
[self _setStateAndResubscribeImageResponseObserver:nullptr];
_imageView.image = nil;
}
#pragma mark - RCTImageResponseDelegate
- (void)didReceiveImage:(UIImage *)image metadata:(id)metadata fromObserver:(const void *)observer
{
if (!_eventEmitter || !_state) {
// Notifications are delivered asynchronously and might arrive after the view is already recycled.
// In the future, we should incorporate an `EventEmitter` into a separate object owned by `ImageRequest` or `State`.
// See for more info: T46311063.
return;
}
static_cast<const ImageEventEmitter &>(*_eventEmitter).onLoad();
static_cast<const ImageEventEmitter &>(*_eventEmitter).onLoadEnd();
const auto &imageProps = static_cast<const ImageProps &>(*_props);
if (imageProps.tintColor) {
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
}
if (imageProps.resizeMode == ImageResizeMode::Repeat) {
image = [image resizableImageWithCapInsets:RCTUIEdgeInsetsFromEdgeInsets(imageProps.capInsets)
resizingMode:UIImageResizingModeTile];
} else if (imageProps.capInsets != EdgeInsets()) {
// Applying capInsets of 0 will switch the "resizingMode" of the image to "tile" which is undesired.
image = [image resizableImageWithCapInsets:RCTUIEdgeInsetsFromEdgeInsets(imageProps.capInsets)
resizingMode:UIImageResizingModeStretch];
}
if (imageProps.blurRadius > __FLT_EPSILON__) {
// Blur on a background thread to avoid blocking interaction.
CGFloat blurRadius = imageProps.blurRadius;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *blurredImage = RCTBlurredImageWithRadius(image, blurRadius);
RCTExecuteOnMainQueue(^{
self->_imageView.image = blurredImage;
});
});
} else {
self->_imageView.image = image;
}
}
- (void)didReceiveProgress:(float)progress fromObserver:(const void *)observer
{
if (!_eventEmitter) {
return;
}
static_cast<const ImageEventEmitter &>(*_eventEmitter).onProgress(progress);
}
- (void)didReceiveFailureFromObserver:(const void *)observer
{
_imageView.image = nil;
if (!_eventEmitter) {
return;
}
static_cast<const ImageEventEmitter &>(*_eventEmitter).onError();
static_cast<const ImageEventEmitter &>(*_eventEmitter).onLoadEnd();
}
@end
#ifdef __cplusplus
extern "C" {
#endif
// Can't the import generated Plugin.h because plugins are not in this BUCK target
Class<RCTComponentViewProtocol> RCTImageCls(void);
#ifdef __cplusplus
}
#endif
Class<RCTComponentViewProtocol> RCTImageCls(void)
{
return RCTImageComponentView.class;
}

View File

@@ -0,0 +1,21 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/**
* UIView class for root <InputAccessoryView> component.
*/
@interface RCTInputAccessoryComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,153 @@
/*
* 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 "RCTInputAccessoryComponentView.h"
#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTConversions.h>
#import <React/RCTSurfaceTouchHandler.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
#import <react/renderer/components/inputaccessory/InputAccessoryComponentDescriptor.h>
#import <react/renderer/components/rncore/Props.h>
#import "RCTInputAccessoryContentView.h"
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
static UIView<RCTBackedTextInputViewProtocol> *_Nullable RCTFindTextInputWithNativeId(UIView *view, NSString *nativeId)
{
if ([view respondsToSelector:@selector(inputAccessoryViewID)] &&
[view respondsToSelector:@selector(setInputAccessoryView:)]) {
UIView<RCTBackedTextInputViewProtocol> *typed = (UIView<RCTBackedTextInputViewProtocol> *)view;
if (!nativeId || [typed.inputAccessoryViewID isEqualToString:nativeId]) {
return typed;
}
}
for (UIView *subview in view.subviews) {
UIView<RCTBackedTextInputViewProtocol> *result = RCTFindTextInputWithNativeId(subview, nativeId);
if (result) {
return result;
}
}
return nil;
}
@implementation RCTInputAccessoryComponentView {
InputAccessoryShadowNode::ConcreteState::Shared _state;
RCTInputAccessoryContentView *_contentView;
RCTSurfaceTouchHandler *_touchHandler;
UIView<RCTBackedTextInputViewProtocol> __weak *_textInput;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = InputAccessoryShadowNode::defaultSharedProps();
_contentView = [RCTInputAccessoryContentView new];
_touchHandler = [RCTSurfaceTouchHandler new];
[_touchHandler attachToView:_contentView];
}
return self;
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (self.window && !_textInput) {
if (self.nativeId) {
_textInput = RCTFindTextInputWithNativeId(self.window, self.nativeId);
_textInput.inputAccessoryView = _contentView;
} else {
_textInput = RCTFindTextInputWithNativeId(_contentView, nil);
}
if (!self.nativeId) {
[self becomeFirstResponder];
}
}
}
- (BOOL)canBecomeFirstResponder
{
return true;
}
- (UIView *)inputAccessoryView
{
return _contentView;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<InputAccessoryComponentDescriptor>();
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_contentView insertSubview:childComponentView atIndex:index];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldInputAccessoryProps = static_cast<const InputAccessoryProps &>(*_props);
const auto &newInputAccessoryProps = static_cast<const InputAccessoryProps &>(*props);
if (newInputAccessoryProps.backgroundColor != oldInputAccessoryProps.backgroundColor) {
_contentView.backgroundColor = RCTUIColorFromSharedColor(newInputAccessoryProps.backgroundColor);
}
[super updateProps:props oldProps:oldProps];
self.hidden = true;
}
- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
_state = std::static_pointer_cast<InputAccessoryShadowNode::ConcreteState const>(state);
CGSize oldScreenSize = RCTCGSizeFromSize(_state->getData().viewportSize);
CGSize viewportSize = RCTViewportSize();
viewportSize.height = std::nan("");
if (oldScreenSize.width != viewportSize.width) {
auto stateData = InputAccessoryState{RCTSizeFromCGSize(viewportSize)};
_state->updateState(std::move(stateData));
}
}
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
[_contentView setFrame:RCTCGRectFromRect(layoutMetrics.getContentFrame())];
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_state.reset();
_textInput = nil;
}
@end
Class<RCTComponentViewProtocol> RCTInputAccessoryCls(void)
{
return RCTInputAccessoryComponentView.class;
}

View File

@@ -0,0 +1,12 @@
/*
* 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 <UIKit/UIKit.h>
@interface RCTInputAccessoryContentView : UIView
@end

View File

@@ -0,0 +1,61 @@
/*
* 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 "RCTInputAccessoryContentView.h"
@implementation RCTInputAccessoryContentView {
UIView *_safeAreaContainer;
NSLayoutConstraint *_heightConstraint;
}
- (instancetype)init
{
if (self = [super init]) {
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
_safeAreaContainer = [UIView new];
_safeAreaContainer.translatesAutoresizingMaskIntoConstraints = NO;
[self addSubview:_safeAreaContainer];
_heightConstraint = [_safeAreaContainer.heightAnchor constraintEqualToConstant:0];
_heightConstraint.active = YES;
[NSLayoutConstraint activateConstraints:@[
[_safeAreaContainer.bottomAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.bottomAnchor],
[_safeAreaContainer.topAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.topAnchor],
[_safeAreaContainer.leadingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.leadingAnchor],
[_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.safeAreaLayoutGuide.trailingAnchor]
]];
}
return self;
}
- (CGSize)intrinsicContentSize
{
// This is needed so the view size is based on autolayout constraints.
return CGSizeZero;
}
- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index
{
[_safeAreaContainer insertSubview:view atIndex:index];
}
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
[_safeAreaContainer setFrame:frame];
_heightConstraint.constant = frame.size.height;
[self layoutIfNeeded];
}
- (BOOL)canBecomeFirstResponder
{
return true;
}
@end

View File

@@ -0,0 +1,32 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTLegacyViewManagerInteropComponentView : RCTViewComponentView
/**
Returns true for components that are supported by LegacyViewManagerInterop layer, false otherwise.
*/
+ (BOOL)isSupported:(NSString *)componentName;
+ (void)supportLegacyViewManagerWithName:(NSString *)componentName;
+ (void)supportLegacyViewManagersWithPrefix:(NSString *)prefix;
/**
* This method is required for addUIBlock and to let the infra bypass the interop layer
* when providing views from the RCTUIManager. The interop layer should be transparent to the users.
*/
- (UIView *)paperView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,278 @@
/*
* 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 "RCTLegacyViewManagerInteropComponentView.h"
#import <React/RCTAssert.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTConstants.h>
#import <React/UIView+React.h>
#import <react/renderer/components/legacyviewmanagerinterop/LegacyViewManagerInteropComponentDescriptor.h>
#import <react/renderer/components/legacyviewmanagerinterop/LegacyViewManagerInteropViewProps.h>
#import <react/utils/ManagedObjectWrapper.h>
#import "RCTLegacyViewManagerInteropCoordinatorAdapter.h"
using namespace facebook::react;
static NSString *const kRCTLegacyInteropChildComponentKey = @"childComponentView";
static NSString *const kRCTLegacyInteropChildIndexKey = @"index";
@implementation RCTLegacyViewManagerInteropComponentView {
NSMutableArray<NSDictionary *> *_viewsToBeMounted;
NSMutableArray<UIView *> *_viewsToBeUnmounted;
RCTLegacyViewManagerInteropCoordinatorAdapter *_adapter;
LegacyViewManagerInteropShadowNode::ConcreteState::Shared _state;
BOOL _hasInvokedForwardingWarning;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = LegacyViewManagerInteropShadowNode::defaultSharedProps();
_viewsToBeMounted = [NSMutableArray new];
_viewsToBeUnmounted = [NSMutableArray new];
_hasInvokedForwardingWarning = NO;
}
return self;
}
- (RCTLegacyViewManagerInteropCoordinator *)_coordinator
{
if (_state != nullptr) {
const auto &state = _state->getData();
return unwrapManagedObject(state.coordinator);
} else {
return nil;
}
}
- (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN
{
const auto &state = _state->getData();
RCTLegacyViewManagerInteropCoordinator *coordinator = unwrapManagedObject(state.coordinator);
return coordinator.componentViewName;
}
#pragma mark - Method forwarding
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if (!_hasInvokedForwardingWarning) {
_hasInvokedForwardingWarning = YES;
NSLog(
@"Invoked unsupported method on RCTLegacyViewManagerInteropComponentView. Resulting to noop instead of a crash.");
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [super methodSignatureForSelector:aSelector] ?: [self.contentView methodSignatureForSelector:aSelector];
}
#pragma mark - Supported ViewManagers
+ (NSMutableSet<NSString *> *)supportedViewManagers
{
static NSMutableSet<NSString *> *supported = [NSMutableSet setWithObjects:@"DatePicker",
@"ProgressView",
@"SegmentedControl",
@"MaskedView",
@"ARTSurfaceView",
@"ARTText",
@"ARTShape",
@"ARTGroup",
nil];
return supported;
}
+ (NSMutableSet<NSString *> *)supportedViewManagersPrefixes
{
static NSMutableSet<NSString *> *supported = [NSMutableSet new];
return supported;
}
+ (NSMutableDictionary<NSString *, Class> *)_supportedLegacyViewComponents
{
static NSMutableDictionary<NSString *, Class> *suppoerted = [NSMutableDictionary new];
return suppoerted;
}
+ (BOOL)isSupported:(NSString *)componentName
{
// Step 1: check if ViewManager with specified name is supported.
BOOL isComponentNameSupported =
[[RCTLegacyViewManagerInteropComponentView supportedViewManagers] containsObject:componentName];
if (isComponentNameSupported) {
return YES;
}
// Step 2: check if component has supported prefix.
for (NSString *item in [RCTLegacyViewManagerInteropComponentView supportedViewManagersPrefixes]) {
if ([componentName hasPrefix:item]) {
return YES;
}
}
// Step 3: check if the module has been registered
// TODO(T174674274): Implement lazy loading of legacy view managers in the new architecture.
NSArray<Class> *registeredModules = RCTGetModuleClasses();
NSMutableDictionary<NSString *, Class> *supportedLegacyViewComponents =
[RCTLegacyViewManagerInteropComponentView _supportedLegacyViewComponents];
if (supportedLegacyViewComponents[componentName] != NULL) {
return YES;
}
for (Class moduleClass in registeredModules) {
id<RCTBridgeModule> bridgeModule = (id<RCTBridgeModule>)moduleClass;
NSString *moduleName = [[bridgeModule moduleName] isEqualToString:@""]
? [NSStringFromClass(moduleClass) stringByReplacingOccurrencesOfString:@"Manager" withString:@""]
: [bridgeModule moduleName];
if (supportedLegacyViewComponents[moduleName] == NULL) {
supportedLegacyViewComponents[moduleName] = moduleClass;
}
if ([moduleName isEqualToString:componentName] ||
[moduleName isEqualToString:[@"RCT" stringByAppendingString:componentName]]) {
return YES;
}
}
return NO;
}
+ (void)supportLegacyViewManagersWithPrefix:(NSString *)prefix
{
[[RCTLegacyViewManagerInteropComponentView supportedViewManagersPrefixes] addObject:prefix];
}
+ (void)supportLegacyViewManagerWithName:(NSString *)componentName
{
[[RCTLegacyViewManagerInteropComponentView supportedViewManagers] addObject:componentName];
}
#pragma mark - RCTComponentViewProtocol
- (void)prepareForRecycle
{
_adapter = nil;
[_viewsToBeMounted removeAllObjects];
[_viewsToBeUnmounted removeAllObjects];
_state.reset();
self.contentView = nil;
_hasInvokedForwardingWarning = NO;
[super prepareForRecycle];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_viewsToBeMounted addObject:@{
kRCTLegacyInteropChildIndexKey : [NSNumber numberWithInteger:index],
kRCTLegacyInteropChildComponentKey : childComponentView
}];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if (_adapter) {
[_adapter.paperView removeReactSubview:childComponentView];
} else {
[_viewsToBeUnmounted addObject:childComponentView];
}
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<LegacyViewManagerInteropComponentDescriptor>();
}
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
{
_state = std::static_pointer_cast<LegacyViewManagerInteropShadowNode::ConcreteState const>(state);
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
__block BOOL propsUpdated = NO;
__weak __typeof(self) weakSelf = self;
void (^updatePropsIfNeeded)(RNComponentViewUpdateMask) = ^void(RNComponentViewUpdateMask mask) {
__typeof(self) strongSelf = weakSelf;
if (!propsUpdated) {
[strongSelf _setPropsWithUpdateMask:mask];
propsUpdated = YES;
}
};
if (!_adapter) {
_adapter = [[RCTLegacyViewManagerInteropCoordinatorAdapter alloc] initWithCoordinator:[self _coordinator]
reactTag:self.tag];
_adapter.eventInterceptor = ^(std::string eventName, folly::dynamic event) {
if (weakSelf) {
__typeof(self) strongSelf = weakSelf;
const auto &eventEmitter =
static_cast<LegacyViewManagerInteropViewEventEmitter const &>(*strongSelf->_eventEmitter);
eventEmitter.dispatchEvent(eventName, event);
}
};
// Set props immediately. This is required to set the initial state of the view.
// In the case where some events are fired in relationship of a change in the frame
// or layout of the view, they will fire as soon as the contentView is set and if the
// event block is nil, the app will crash.
updatePropsIfNeeded(updateMask);
propsUpdated = YES;
self.contentView = _adapter.paperView;
}
for (NSDictionary *mountInstruction in _viewsToBeMounted) {
NSNumber *index = mountInstruction[kRCTLegacyInteropChildIndexKey];
UIView *childView = mountInstruction[kRCTLegacyInteropChildComponentKey];
if ([childView isKindOfClass:[RCTLegacyViewManagerInteropComponentView class]]) {
UIView *target = ((RCTLegacyViewManagerInteropComponentView *)childView).contentView;
[_adapter.paperView insertReactSubview:target atIndex:index.integerValue];
} else {
[_adapter.paperView insertReactSubview:childView atIndex:index.integerValue];
}
}
[_viewsToBeMounted removeAllObjects];
for (UIView *view in _viewsToBeUnmounted) {
[_adapter.paperView removeReactSubview:view];
}
[_viewsToBeUnmounted removeAllObjects];
[_adapter.paperView didUpdateReactSubviews];
updatePropsIfNeeded(updateMask);
}
- (void)_setPropsWithUpdateMask:(RNComponentViewUpdateMask)updateMask
{
if (updateMask & RNComponentViewUpdateMaskProps) {
const auto &newProps = static_cast<const LegacyViewManagerInteropViewProps &>(*_props);
[_adapter setProps:newProps.otherProps];
}
}
- (UIView *)paperView
{
return _adapter.paperView;
}
#pragma mark - Native Commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
[_adapter handleCommand:(NSString *)commandName args:(NSArray *)args];
}
@end

View File

@@ -0,0 +1,27 @@
/*
* 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 <Foundation/Foundation.h>
#import <react/renderer/components/legacyviewmanagerinterop/RCTLegacyViewManagerInteropCoordinator.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTLegacyViewManagerInteropCoordinatorAdapter : NSObject
- (instancetype)initWithCoordinator:(RCTLegacyViewManagerInteropCoordinator *)coordinator reactTag:(NSInteger)tag;
@property (strong, nonatomic) UIView *paperView;
@property (nonatomic, copy, nullable) void (^eventInterceptor)(std::string eventName, folly::dynamic event);
- (void)setProps:(const folly::dynamic &)props;
- (void)handleCommand:(NSString *)commandName args:(NSArray *)args;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,148 @@
/*
* 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 "RCTLegacyViewManagerInteropCoordinatorAdapter.h"
#import <React/RCTFollyConvert.h>
#import <React/UIView+React.h>
@implementation RCTLegacyViewManagerInteropCoordinatorAdapter {
RCTLegacyViewManagerInteropCoordinator *_coordinator;
NSInteger _tag;
NSDictionary<NSString *, id> *_oldProps;
}
- (instancetype)initWithCoordinator:(RCTLegacyViewManagerInteropCoordinator *)coordinator reactTag:(NSInteger)tag
{
if (self = [super init]) {
_coordinator = coordinator;
_tag = tag;
}
return self;
}
- (void)dealloc
{
[_paperView removeFromSuperview];
[_coordinator removeObserveForTag:_tag];
}
- (UIView *)paperView
{
if (!_paperView) {
_paperView = [_coordinator createPaperViewWithTag:_tag];
__weak __typeof(self) weakSelf = self;
[_coordinator addObserveForTag:_tag
usingBlock:^(std::string eventName, folly::dynamic event) {
if (weakSelf.eventInterceptor) {
weakSelf.eventInterceptor(eventName, event);
}
}];
}
return _paperView;
}
- (void)setProps:(const folly::dynamic &)props
{
if (props.isObject()) {
NSDictionary<NSString *, id> *convertedProps = facebook::react::convertFollyDynamicToId(props);
NSDictionary<NSString *, id> *diffedProps = [self _diffProps:convertedProps];
[_coordinator setProps:diffedProps forView:self.paperView];
_oldProps = convertedProps;
}
}
- (void)handleCommand:(NSString *)commandName args:(NSArray *)args
{
[_coordinator handleCommand:commandName args:args reactTag:_tag paperView:self.paperView];
}
- (NSDictionary<NSString *, id> *)_diffProps:(NSDictionary<NSString *, id> *)newProps
{
NSMutableDictionary<NSString *, id> *diffedProps = [NSMutableDictionary new];
[newProps enumerateKeysAndObjectsUsingBlock:^(NSString *key, id newProp, __unused BOOL *stop) {
id oldProp = _oldProps[key];
if ([self _prop:newProp isDifferentFrom:oldProp]) {
diffedProps[key] = newProp;
}
}];
return diffedProps;
}
#pragma mark - Private
- (BOOL)_prop:(id)oldProp isDifferentFrom:(id)newProp
{
// Check for JSON types.
// JSON types can be of:
// * number
// * bool
// * String
// * Array
// * Objects => Dictionaries in ObjectiveC
// * Null
// Check for NULL
BOOL bothNil = !oldProp && !newProp;
if (bothNil) {
return NO;
}
BOOL onlyOneNil = (oldProp && !newProp) || (!oldProp && newProp);
if (onlyOneNil) {
return YES;
}
if ([self _propIsSameNumber:oldProp second:newProp]) {
// Boolean should be captured by NSNumber
return NO;
}
if ([self _propIsSameString:oldProp second:newProp]) {
return NO;
}
if ([self _propIsSameArray:oldProp second:newProp]) {
return NO;
}
if ([self _propIsSameObject:oldProp second:newProp]) {
return NO;
}
// Previous behavior, fallback to YES
return YES;
}
- (BOOL)_propIsSameNumber:(id)first second:(id)second
{
return [first isKindOfClass:[NSNumber class]] && [second isKindOfClass:[NSNumber class]] &&
[(NSNumber *)first isEqualToNumber:(NSNumber *)second];
}
- (BOOL)_propIsSameString:(id)first second:(id)second
{
return [first isKindOfClass:[NSString class]] && [second isKindOfClass:[NSString class]] &&
[(NSString *)first isEqualToString:(NSString *)second];
}
- (BOOL)_propIsSameArray:(id)first second:(id)second
{
return [first isKindOfClass:[NSArray class]] && [second isKindOfClass:[NSArray class]] &&
[(NSArray *)first isEqualToArray:(NSArray *)second];
}
- (BOOL)_propIsSameObject:(id)first second:(id)second
{
return [first isKindOfClass:[NSDictionary class]] && [second isKindOfClass:[NSDictionary class]] &&
[(NSDictionary *)first isEqualToDictionary:(NSDictionary *)second];
}
@end

View File

@@ -0,0 +1,20 @@
/*
* 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 <UIKit/UIKit.h>
@protocol RCTFabricModalHostViewControllerDelegate <NSObject>
- (void)boundsDidChange:(CGRect)newBounds;
@end
@interface RCTFabricModalHostViewController : UIViewController
@property (nonatomic, weak) id<RCTFabricModalHostViewControllerDelegate> delegate;
@property (nonatomic, assign) UIInterfaceOrientationMask supportedInterfaceOrientations;
@end

View File

@@ -0,0 +1,80 @@
/*
* 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 "RCTFabricModalHostViewController.h"
#import <React/RCTLog.h>
#import <React/RCTSurfaceTouchHandler.h>
@implementation RCTFabricModalHostViewController {
CGRect _lastViewBounds;
RCTSurfaceTouchHandler *_touchHandler;
}
- (instancetype)init
{
if (!(self = [super init])) {
return nil;
}
_touchHandler = [RCTSurfaceTouchHandler new];
self.modalInPresentation = YES;
return self;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!CGRectEqualToRect(_lastViewBounds, self.view.bounds)) {
[_delegate boundsDidChange:self.view.bounds];
_lastViewBounds = self.view.bounds;
}
}
- (void)loadView
{
[super loadView];
[_touchHandler attachToView:self.view];
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
return [RCTUIStatusBarManager() statusBarStyle];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
_lastViewBounds = CGRectZero;
}
- (BOOL)prefersStatusBarHidden
{
return [RCTUIStatusBarManager() isStatusBarHidden];
}
#if RCT_DEV
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
UIInterfaceOrientationMask appSupportedOrientationsMask =
[RCTSharedApplication() supportedInterfaceOrientationsForWindow:[RCTSharedApplication() keyWindow]];
if (!(_supportedInterfaceOrientations & appSupportedOrientationsMask)) {
RCTLogError(
@"Modal was presented with 0x%x orientations mask but the application only supports 0x%x."
@"Add more interface orientations to your app's Info.plist to fix this."
@"NOTE: This will crash in non-dev mode.",
(unsigned)_supportedInterfaceOrientations,
(unsigned)appSupportedOrientationsMask);
return UIInterfaceOrientationMaskAll;
}
return _supportedInterfaceOrientations;
}
#endif // RCT_DEV
@end

View File

@@ -0,0 +1,32 @@
/*
* 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/RCTMountingTransactionObserving.h>
#import <React/RCTViewComponentView.h>
/**
* UIView class for root <ModalHostView> component.
*/
@interface RCTModalHostViewComponentView : RCTViewComponentView <RCTMountingTransactionObserving>
/**
* Subclasses may override this method and present the modal on different view controller.
* Default implementation presents the modal on `[self reactViewController]`.
*/
- (void)presentViewController:(UIViewController *)modalViewController
animated:(BOOL)animated
completion:(void (^)(void))completion;
/**
* Subclasses may override this method.
* Default implementation calls `[UIViewController dismissViewControllerAnimated:completion:]`.
*/
- (void)dismissViewController:(UIViewController *)modalViewController
animated:(BOOL)animated
completion:(void (^)(void))completion;
@end

View File

@@ -0,0 +1,300 @@
/*
* 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 "RCTModalHostViewComponentView.h"
#import <React/RCTBridge+Private.h>
#import <React/RCTModalManager.h>
#import <React/UIView+React.h>
#import <react/renderer/components/modal/ModalHostViewComponentDescriptor.h>
#import <react/renderer/components/modal/ModalHostViewState.h>
#import <react/renderer/components/rncore/EventEmitters.h>
#import <react/renderer/components/rncore/Props.h>
#import "RCTConversions.h"
#import "RCTFabricModalHostViewController.h"
using namespace facebook::react;
static UIInterfaceOrientationMask supportedOrientationsMask(ModalHostViewSupportedOrientationsMask mask)
{
UIInterfaceOrientationMask supportedOrientations = 0;
if (mask & ModalHostViewSupportedOrientations::Portrait) {
supportedOrientations |= UIInterfaceOrientationMaskPortrait;
}
if (mask & ModalHostViewSupportedOrientations::PortraitUpsideDown) {
supportedOrientations |= UIInterfaceOrientationMaskPortraitUpsideDown;
}
if (mask & ModalHostViewSupportedOrientations::Landscape) {
supportedOrientations |= UIInterfaceOrientationMaskLandscape;
}
if (mask & ModalHostViewSupportedOrientations::LandscapeLeft) {
supportedOrientations |= UIInterfaceOrientationMaskLandscapeLeft;
}
if (mask & ModalHostViewSupportedOrientations::LandscapeRight) {
supportedOrientations |= UIInterfaceOrientationMaskLandscapeRight;
}
if (supportedOrientations == 0) {
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
return UIInterfaceOrientationMaskAll;
} else {
return UIInterfaceOrientationMaskPortrait;
}
}
return supportedOrientations;
}
static std::tuple<BOOL, UIModalTransitionStyle> animationConfiguration(const ModalHostViewAnimationType animation)
{
switch (animation) {
case ModalHostViewAnimationType::None:
return std::make_tuple(NO, UIModalTransitionStyleCoverVertical);
case ModalHostViewAnimationType::Slide:
return std::make_tuple(YES, UIModalTransitionStyleCoverVertical);
case ModalHostViewAnimationType::Fade:
return std::make_tuple(YES, UIModalTransitionStyleCrossDissolve);
}
}
static UIModalPresentationStyle presentationConfiguration(const ModalHostViewProps &props)
{
if (props.transparent) {
return UIModalPresentationOverFullScreen;
}
switch (props.presentationStyle) {
case ModalHostViewPresentationStyle::FullScreen:
return UIModalPresentationFullScreen;
case ModalHostViewPresentationStyle::PageSheet:
return UIModalPresentationPageSheet;
case ModalHostViewPresentationStyle::FormSheet:
return UIModalPresentationFormSheet;
case ModalHostViewPresentationStyle::OverFullScreen:
return UIModalPresentationOverFullScreen;
}
}
static ModalHostViewEventEmitter::OnOrientationChange onOrientationChangeStruct(CGRect rect)
{
;
auto orientation = rect.size.width < rect.size.height
? ModalHostViewEventEmitter::OnOrientationChangeOrientation::Portrait
: ModalHostViewEventEmitter::OnOrientationChangeOrientation::Landscape;
return {orientation};
}
@interface RCTModalHostViewComponentView () <RCTFabricModalHostViewControllerDelegate>
@end
@implementation RCTModalHostViewComponentView {
RCTFabricModalHostViewController *_viewController;
ModalHostViewShadowNode::ConcreteState::Shared _state;
BOOL _shouldAnimatePresentation;
BOOL _shouldPresent;
BOOL _isPresented;
UIView *_modalContentsSnapshot;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ModalHostViewShadowNode::defaultSharedProps();
_shouldAnimatePresentation = YES;
_isPresented = NO;
}
return self;
}
- (RCTFabricModalHostViewController *)viewController
{
if (!_viewController) {
_viewController = [RCTFabricModalHostViewController new];
_viewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
_viewController.delegate = self;
}
return _viewController;
}
- (void)presentViewController:(UIViewController *)modalViewController
animated:(BOOL)animated
completion:(void (^)(void))completion
{
UIViewController *controller = [self reactViewController];
[controller presentViewController:modalViewController animated:animated completion:completion];
}
- (void)dismissViewController:(UIViewController *)modalViewController
animated:(BOOL)animated
completion:(void (^)(void))completion
{
[modalViewController dismissViewControllerAnimated:animated completion:completion];
}
- (void)ensurePresentedOnlyIfNeeded
{
BOOL shouldBePresented = !_isPresented && _shouldPresent && self.window;
if (shouldBePresented) {
_isPresented = YES;
[self presentViewController:self.viewController
animated:_shouldAnimatePresentation
completion:^{
auto eventEmitter = [self modalEventEmitter];
if (eventEmitter) {
eventEmitter->onShow(ModalHostViewEventEmitter::OnShow{});
}
}];
}
BOOL shouldBeHidden = _isPresented && (!_shouldPresent || !self.superview);
if (shouldBeHidden) {
_isPresented = NO;
// To animate dismissal of view controller, snapshot of
// view hierarchy needs to be added to the UIViewController.
UIView *snapshot = _modalContentsSnapshot;
if (_shouldPresent) {
[self.viewController.view addSubview:snapshot];
}
[self dismissViewController:self.viewController
animated:_shouldAnimatePresentation
completion:^{
[snapshot removeFromSuperview];
auto eventEmitter = [self modalEventEmitter];
if (eventEmitter) {
eventEmitter->onDismiss(ModalHostViewEventEmitter::OnDismiss{});
}
}];
}
}
- (std::shared_ptr<const ModalHostViewEventEmitter>)modalEventEmitter
{
if (!_eventEmitter) {
return nullptr;
}
assert(std::dynamic_pointer_cast<const ModalHostViewEventEmitter>(_eventEmitter));
return std::static_pointer_cast<const ModalHostViewEventEmitter>(_eventEmitter);
}
#pragma mark - RCTMountingTransactionObserving
- (void)mountingTransactionWillMount:(const MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
_modalContentsSnapshot = [self.viewController.view snapshotViewAfterScreenUpdates:YES];
}
#pragma mark - UIView methods
- (void)didMoveToWindow
{
[super didMoveToWindow];
[self ensurePresentedOnlyIfNeeded];
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self ensurePresentedOnlyIfNeeded];
}
#pragma mark - RCTFabricModalHostViewControllerDelegate
- (void)boundsDidChange:(CGRect)newBounds
{
auto eventEmitter = [self modalEventEmitter];
if (eventEmitter) {
eventEmitter->onOrientationChange(onOrientationChangeStruct(newBounds));
}
if (_state != nullptr) {
auto newState = ModalHostViewState{RCTSizeFromCGSize(newBounds.size)};
_state->updateState(std::move(newState));
}
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<ModalHostViewComponentDescriptor>();
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_state.reset();
_viewController = nil;
_isPresented = NO;
_shouldPresent = NO;
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &newProps = static_cast<const ModalHostViewProps &>(*props);
#if !TARGET_OS_TV
self.viewController.supportedInterfaceOrientations = supportedOrientationsMask(newProps.supportedOrientations);
#endif
const auto [shouldAnimate, transitionStyle] = animationConfiguration(newProps.animationType);
_shouldAnimatePresentation = shouldAnimate;
self.viewController.modalTransitionStyle = transitionStyle;
self.viewController.modalPresentationStyle = presentationConfiguration(newProps);
_shouldPresent = newProps.visible;
[self ensurePresentedOnlyIfNeeded];
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
_state = std::static_pointer_cast<const ModalHostViewShadowNode::ConcreteState>(state);
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[self.viewController.view insertSubview:childComponentView atIndex:index];
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
}
@end
#ifdef __cplusplus
extern "C" {
#endif
// Can't the import generated Plugin.h because plugins are not in this BUCK target
Class<RCTComponentViewProtocol> RCTModalHostViewCls(void);
#ifdef __cplusplus
}
#endif
Class<RCTComponentViewProtocol> RCTModalHostViewCls(void)
{
return RCTModalHostViewComponentView.class;
}

View File

@@ -0,0 +1,53 @@
/**
* 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.
*
* @generated by an internal plugin build system
*/
#ifdef RN_DISABLE_OSS_PLUGIN_HEADER
// FB Internal: FBRCTFabricComponentsPlugins.h is autogenerated by the build system.
#import <React/FBRCTFabricComponentsPlugins.h>
#else
// OSS-compatibility layer
#import <Foundation/Foundation.h>
#import <React/RCTThirdPartyFabricComponentsProvider.h>
#import <React/RCTComponentViewProtocol.h>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wreturn-type-c-linkage"
#ifdef __cplusplus
extern "C" {
#endif
Class<RCTComponentViewProtocol> RCTFabricComponentsProvider(const char *name);
// Lookup functions
Class<RCTComponentViewProtocol> RCTActivityIndicatorViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTDebuggingOverlayCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTInputAccessoryCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTParagraphCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTPullToRefreshViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTSafeAreaViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTScrollViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTSwitchCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTTextInputCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTUnimplementedNativeViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTViewCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTImageCls(void) __attribute__((used));
Class<RCTComponentViewProtocol> RCTModalHostViewCls(void) __attribute__((used));
#ifdef __cplusplus
}
#endif
#pragma GCC diagnostic pop
#endif // RN_DISABLE_OSS_PLUGIN_HEADER

View File

@@ -0,0 +1,44 @@
/**
* 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.
*
* @generated by an internal plugin build system
*/
#ifndef RN_DISABLE_OSS_PLUGIN_HEADER
// OSS-compatibility layer
#import "RCTFabricComponentsPlugins.h"
#import <string>
#import <unordered_map>
Class<RCTComponentViewProtocol> RCTFabricComponentsProvider(const char *name) {
static std::unordered_map<std::string, Class (*)(void)> sFabricComponentsClassMap = {
{"ActivityIndicatorView", RCTActivityIndicatorViewCls},
{"DebuggingOverlay", RCTDebuggingOverlayCls},
{"InputAccessoryView", RCTInputAccessoryCls},
{"Paragraph", RCTParagraphCls},
{"PullToRefreshView", RCTPullToRefreshViewCls},
{"SafeAreaView", RCTSafeAreaViewCls},
{"ScrollView", RCTScrollViewCls},
{"Switch", RCTSwitchCls},
{"TextInput", RCTTextInputCls},
{"UnimplementedNativeView", RCTUnimplementedNativeViewCls},
{"View", RCTViewCls},
{"Image", RCTImageCls},
{"ModalHostView", RCTModalHostViewCls},
};
auto p = sFabricComponentsClassMap.find(name);
if (p != sFabricComponentsClassMap.end()) {
auto classFunc = p->second;
return classFunc();
}
return RCTThirdPartyFabricComponentsProvider(name);
}
#endif // RN_DISABLE_OSS_PLUGIN_HEADER

View File

@@ -0,0 +1,21 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/**
* UIView class for root <View> component.
*/
@interface RCTRootComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,34 @@
/*
* 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 "RCTRootComponentView.h"
#import <react/renderer/components/root/RootComponentDescriptor.h>
#import <react/renderer/components/root/RootProps.h>
#import "RCTConversions.h"
using namespace facebook::react;
@implementation RCTRootComponentView
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = RootShadowNode::defaultSharedProps();
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<RootComponentDescriptor>();
}
@end

View File

@@ -0,0 +1,21 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/**
* UIView class for root <SafeAreaView> component.
*/
@interface RCTSafeAreaViewComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,100 @@
/*
* 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 "RCTSafeAreaViewComponentView.h"
#import <React/RCTUtils.h>
#import <react/renderer/components/safeareaview/SafeAreaViewComponentDescriptor.h>
#import <react/renderer/components/safeareaview/SafeAreaViewState.h>
#import "RCTConversions.h"
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@implementation RCTSafeAreaViewComponentView {
SafeAreaViewShadowNode::ConcreteState::Shared _state;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = SafeAreaViewShadowNode::defaultSharedProps();
}
return self;
}
- (void)safeAreaInsetsDidChange
{
[super safeAreaInsetsDidChange];
[self _updateStateIfNecessary];
}
- (void)_updateStateIfNecessary
{
if (!_state) {
return;
}
UIEdgeInsets insets = self.safeAreaInsets;
insets.left = RCTRoundPixelValue(insets.left);
insets.top = RCTRoundPixelValue(insets.top);
insets.right = RCTRoundPixelValue(insets.right);
insets.bottom = RCTRoundPixelValue(insets.bottom);
auto newPadding = RCTEdgeInsetsFromUIEdgeInsets(insets);
auto threshold = 1.0 / RCTScreenScale() + 0.01; // Size of a pixel plus some small threshold.
_state->updateState(
[=](const SafeAreaViewShadowNode::ConcreteState::Data &oldData)
-> SafeAreaViewShadowNode::ConcreteState::SharedData {
auto oldPadding = oldData.padding;
auto deltaPadding = newPadding - oldPadding;
if (std::abs(deltaPadding.left) < threshold && std::abs(deltaPadding.top) < threshold &&
std::abs(deltaPadding.right) < threshold && std::abs(deltaPadding.bottom) < threshold) {
return nullptr;
}
auto newData = oldData;
newData.padding = newPadding;
return std::make_shared<SafeAreaViewShadowNode::ConcreteState::Data const>(newData);
});
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<SafeAreaViewComponentDescriptor>();
}
- (void)updateState:(const facebook::react::State::Shared &)state
oldState:(const facebook::react::State::Shared &)oldState
{
_state = std::static_pointer_cast<SafeAreaViewShadowNode::ConcreteState const>(state);
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
[self _updateStateIfNecessary];
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_state.reset();
}
@end
Class<RCTComponentViewProtocol> RCTSafeAreaViewCls(void)
{
return RCTSafeAreaViewComponentView.class;
}

View File

@@ -0,0 +1,15 @@
/*
* 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 <UIKit/UIKit.h>
/**
* Denotes a view which implements custom pull to refresh functionality.
*/
@protocol RCTCustomPullToRefreshViewProtocol
@end

View File

@@ -0,0 +1,63 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTGenericDelegateSplitter.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/*
* Many `UIScrollView` customizations normally require creating a subclass which is not always convenient.
* `RCTEnhancedScrollView` has a delegate (conforming to this protocol) that allows customizing such behaviors without
* creating a subclass.
*/
@protocol RCTEnhancedScrollViewOverridingDelegate <NSObject>
- (BOOL)touchesShouldCancelInContentView:(UIView *)view;
@end
/*
* `UIScrollView` subclass which has some improvements and tweaks
* which are not directly related to React Native.
*/
@interface RCTEnhancedScrollView : UIScrollView
/*
* Returns a delegate splitter that can be used to create as many `UIScrollView` delegates as needed.
* Use that instead of accessing `delegate` property directly.
*
* This class overrides the `delegate` property and wires that to the delegate splitter.
*
* We never know which another part of the app might introspect the view hierarchy and mess with `UIScrollView`'s
* delegate, so we expose a fake delegate connected to the original one via the splitter to make the component as
* resilient to other code as possible: even if something else nil the delegate, other delegates that were subscribed
* via the splitter will continue working.
*/
@property (nonatomic, strong, readonly) RCTGenericDelegateSplitter<id<UIScrollViewDelegate>> *delegateSplitter;
@property (nonatomic, weak) id<RCTEnhancedScrollViewOverridingDelegate> overridingDelegate;
@property (nonatomic, assign) BOOL pinchGestureEnabled;
@property (nonatomic, assign) BOOL centerContent;
@property (nonatomic, assign) CGFloat snapToInterval;
@property (nonatomic, copy) NSString *snapToAlignment;
@property (nonatomic, assign) BOOL disableIntervalMomentum;
@property (nonatomic, assign) BOOL snapToStart;
@property (nonatomic, assign) BOOL snapToEnd;
@property (nonatomic, copy) NSArray<NSNumber *> *snapToOffsets;
/*
* Makes `setContentOffset:` method no-op when given `block` is executed.
* The block is being executed synchronously.
*/
- (void)preserveContentOffsetWithBlock:(void (^)())block;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,267 @@
/*
* 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 "RCTEnhancedScrollView.h"
#import <React/RCTUtils.h>
@interface RCTEnhancedScrollView () <UIScrollViewDelegate>
@end
@implementation RCTEnhancedScrollView {
__weak id<UIScrollViewDelegate> _publicDelegate;
BOOL _isSetContentOffsetDisabled;
}
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
if ([key isEqualToString:@"delegate"]) {
// For `delegate` property, we issue KVO notifications manually.
// We need that to block notifications caused by setting the original `UIScrollView`s property.
return NO;
}
return [super automaticallyNotifiesObserversForKey:key];
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// We set the default behavior to "never" so that iOS
// doesn't do weird things to UIScrollView insets automatically
// and keeps it as an opt-in behavior.
self.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
// We intentionally force `UIScrollView`s `semanticContentAttribute` to `LTR` here
// because this attribute affects a position of vertical scrollbar; we don't want this
// scrollbar flip because we also flip it with whole `UIScrollView` flip.
self.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
__weak __typeof(self) weakSelf = self;
_delegateSplitter = [[RCTGenericDelegateSplitter alloc] initWithDelegateUpdateBlock:^(id delegate) {
[weakSelf setPrivateDelegate:delegate];
}];
[_delegateSplitter addDelegate:self];
}
return self;
}
- (void)preserveContentOffsetWithBlock:(void (^)())block
{
if (!block) {
return;
}
_isSetContentOffsetDisabled = YES;
block();
_isSetContentOffsetDisabled = NO;
}
/*
* Automatically centers the content such that if the content is smaller than the
* ScrollView, we force it to be centered, but when you zoom or the content otherwise
* becomes larger than the ScrollView, there is no padding around the content but it
* can still fill the whole view.
*/
- (void)setContentOffset:(CGPoint)contentOffset
{
if (_isSetContentOffsetDisabled) {
return;
}
if (_centerContent && !CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
CGSize scrollViewSize = self.bounds.size;
if (self.contentSize.width <= scrollViewSize.width) {
contentOffset.x = -(scrollViewSize.width - self.contentSize.width) / 2.0;
}
if (self.contentSize.height <= scrollViewSize.height) {
contentOffset.y = -(scrollViewSize.height - self.contentSize.height) / 2.0;
}
}
super.contentOffset = CGPointMake(
RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
if ([_overridingDelegate respondsToSelector:@selector(touchesShouldCancelInContentView:)]) {
return [_overridingDelegate touchesShouldCancelInContentView:view];
}
return [super touchesShouldCancelInContentView:view];
}
#pragma mark - RCTGenericDelegateSplitter
- (void)setPrivateDelegate:(id<UIScrollViewDelegate>)delegate
{
[super setDelegate:delegate];
}
- (id<UIScrollViewDelegate>)delegate
{
return _publicDelegate;
}
- (void)setDelegate:(id<UIScrollViewDelegate>)delegate
{
if (_publicDelegate == delegate) {
return;
}
if (_publicDelegate) {
[_delegateSplitter removeDelegate:_publicDelegate];
}
[self willChangeValueForKey:@"delegate"];
_publicDelegate = delegate;
[self didChangeValueForKey:@"delegate"];
if (_publicDelegate) {
[_delegateSplitter addDelegate:_publicDelegate];
}
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
if (self.snapToOffsets && self.snapToOffsets.count > 0) {
// An alternative to enablePaging and snapToInterval which allows setting custom
// stopping points that don't have to be the same distance apart. Often seen in
// apps which feature horizonally scrolling items. snapToInterval does not enforce
// scrolling one interval at a time but guarantees that the scroll will stop at
// a snap offset point.
// Find which axis to snap
BOOL isHorizontal = [self isHorizontal:scrollView];
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
CGFloat offsetAlongAxis = isHorizontal ? scrollView.contentOffset.x : scrollView.contentOffset.y;
// Calculate maximum content offset
CGSize viewportSize = self.bounds.size;
CGFloat maximumOffset = isHorizontal ? MAX(0, scrollView.contentSize.width - viewportSize.width)
: MAX(0, scrollView.contentSize.height - viewportSize.height);
// Calculate the snap offsets adjacent to the initial offset target
CGFloat targetOffset = isHorizontal ? targetContentOffset->x : targetContentOffset->y;
CGFloat smallerOffset = 0.0;
CGFloat largerOffset = maximumOffset;
for (unsigned long i = 0; i < self.snapToOffsets.count; i++) {
CGFloat offset = [[self.snapToOffsets objectAtIndex:i] floatValue];
if (offset <= targetOffset) {
if (targetOffset - offset < targetOffset - smallerOffset) {
smallerOffset = offset;
}
}
if (offset >= targetOffset) {
if (offset - targetOffset < largerOffset - targetOffset) {
largerOffset = offset;
}
}
}
// Calculate the nearest offset
CGFloat nearestOffset = targetOffset - smallerOffset < largerOffset - targetOffset ? smallerOffset : largerOffset;
CGFloat firstOffset = [[self.snapToOffsets firstObject] floatValue];
CGFloat lastOffset = [[self.snapToOffsets lastObject] floatValue];
// if scrolling after the last snap offset and snapping to the
// end of the list is disabled, then we allow free scrolling
if (!self.snapToEnd && targetOffset >= lastOffset) {
if (offsetAlongAxis >= lastOffset) {
// free scrolling
} else {
// snap to end
targetOffset = lastOffset;
}
} else if (!self.snapToStart && targetOffset <= firstOffset) {
if (offsetAlongAxis <= firstOffset) {
// free scrolling
} else {
// snap to beginning
targetOffset = firstOffset;
}
} else if (velocityAlongAxis > 0.0) {
targetOffset = largerOffset;
} else if (velocityAlongAxis < 0.0) {
targetOffset = smallerOffset;
} else {
targetOffset = nearestOffset;
}
// Make sure the new offset isn't out of bounds
targetOffset = MIN(MAX(0, targetOffset), maximumOffset);
// Set new targetContentOffset
if (isHorizontal) {
targetContentOffset->x = targetOffset;
} else {
targetContentOffset->y = targetOffset;
}
} else if (self.snapToInterval) {
// An alternative to enablePaging which allows setting custom stopping intervals,
// smaller than a full page size. Often seen in apps which feature horizonally
// scrolling items. snapToInterval does not enforce scrolling one interval at a time
// but guarantees that the scroll will stop at an interval point.
CGFloat snapToIntervalF = (CGFloat)self.snapToInterval;
// Find which axis to snap
BOOL isHorizontal = [self isHorizontal:scrollView];
// What is the current offset?
CGFloat velocityAlongAxis = isHorizontal ? velocity.x : velocity.y;
CGFloat targetContentOffsetAlongAxis = targetContentOffset->y;
if (isHorizontal) {
// Use current scroll offset to determine the next index to snap to when momentum disabled
targetContentOffsetAlongAxis = self.disableIntervalMomentum ? scrollView.contentOffset.x : targetContentOffset->x;
} else {
targetContentOffsetAlongAxis = self.disableIntervalMomentum ? scrollView.contentOffset.y : targetContentOffset->y;
}
// Offset based on desired alignment
CGFloat frameLength = isHorizontal ? self.frame.size.width : self.frame.size.height;
CGFloat alignmentOffset = 0.0f;
if ([self.snapToAlignment isEqualToString:@"center"]) {
alignmentOffset = (frameLength * 0.5f) + (snapToIntervalF * 0.5f);
} else if ([self.snapToAlignment isEqualToString:@"end"]) {
alignmentOffset = frameLength;
}
// Pick snap point based on direction and proximity
CGFloat fractionalIndex = (targetContentOffsetAlongAxis + alignmentOffset) / snapToIntervalF;
NSInteger snapIndex = velocityAlongAxis > 0.0 ? ceil(fractionalIndex)
: velocityAlongAxis < 0.0 ? floor(fractionalIndex)
: round(fractionalIndex);
CGFloat newTargetContentOffset = ((CGFloat)snapIndex * snapToIntervalF) - alignmentOffset;
// Set new targetContentOffset
if (isHorizontal) {
targetContentOffset->x = newTargetContentOffset;
} else {
targetContentOffset->y = newTargetContentOffset;
}
}
}
#pragma mark -
- (BOOL)isHorizontal:(UIScrollView *)scrollView
{
return scrollView.contentSize.width > self.frame.size.width;
}
@end

View File

@@ -0,0 +1,24 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTCustomPullToRefreshViewProtocol.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/*
* UIView class for root <PullToRefreshView> component.
* This view is designed to only serve ViewController-like purpose for the actual `UIRefreshControl` view which is being
* attached to some `UIScrollView` (not to this view).
*/
@interface RCTPullToRefreshViewComponentView : RCTViewComponentView <RCTCustomPullToRefreshViewProtocol>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,189 @@
/*
* 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 "RCTPullToRefreshViewComponentView.h"
#import <react/renderer/components/rncore/ComponentDescriptors.h>
#import <react/renderer/components/rncore/EventEmitters.h>
#import <react/renderer/components/rncore/Props.h>
#import <react/renderer/components/rncore/RCTComponentViewHelpers.h>
#import <React/RCTConversions.h>
#import <React/RCTRefreshableProtocol.h>
#import <React/RCTScrollViewComponentView.h>
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@interface RCTPullToRefreshViewComponentView () <RCTPullToRefreshViewViewProtocol, RCTRefreshableProtocol>
@end
@implementation RCTPullToRefreshViewComponentView {
UIRefreshControl *_refreshControl;
RCTScrollViewComponentView *__weak _scrollViewComponentView;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
// This view is not designed to be visible, it only serves UIViewController-like purpose managing
// attaching and detaching of a pull-to-refresh view to a scroll view.
// The pull-to-refresh view is not a subview of this view.
self.hidden = YES;
_props = PullToRefreshViewShadowNode::defaultSharedProps();
_refreshControl = [UIRefreshControl new];
[_refreshControl addTarget:self
action:@selector(handleUIControlEventValueChanged)
forControlEvents:UIControlEventValueChanged];
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<PullToRefreshViewComponentDescriptor>();
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldConcreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
const auto &newConcreteProps = static_cast<const PullToRefreshViewProps &>(*props);
if (newConcreteProps.refreshing != oldConcreteProps.refreshing) {
if (newConcreteProps.refreshing) {
[_refreshControl beginRefreshing];
} else {
[_refreshControl endRefreshing];
}
}
BOOL needsUpdateTitle = NO;
if (newConcreteProps.title != oldConcreteProps.title) {
needsUpdateTitle = YES;
}
if (newConcreteProps.titleColor != oldConcreteProps.titleColor) {
needsUpdateTitle = YES;
}
if (needsUpdateTitle) {
[self _updateTitle];
}
[super updateProps:props oldProps:oldProps];
}
#pragma mark -
- (void)handleUIControlEventValueChanged
{
static_cast<const PullToRefreshViewEventEmitter &>(*_eventEmitter).onRefresh({});
}
- (void)_updateTitle
{
const auto &concreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
if (concreteProps.title.empty()) {
_refreshControl.attributedTitle = nil;
return;
}
NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
if (concreteProps.titleColor) {
attributes[NSForegroundColorAttributeName] = RCTUIColorFromSharedColor(concreteProps.titleColor);
}
_refreshControl.attributedTitle =
[[NSAttributedString alloc] initWithString:RCTNSStringFromString(concreteProps.title) attributes:attributes];
}
#pragma mark - Attaching & Detaching
- (void)didMoveToWindow
{
if (self.window) {
[self _attach];
} else {
[self _detach];
}
}
- (void)_attach
{
if (_scrollViewComponentView) {
[self _detach];
}
_scrollViewComponentView = [RCTScrollViewComponentView findScrollViewComponentViewForView:self];
if (!_scrollViewComponentView) {
return;
}
if (@available(macCatalyst 13.1, *)) {
_scrollViewComponentView.scrollView.refreshControl = _refreshControl;
}
}
- (void)_detach
{
if (!_scrollViewComponentView) {
return;
}
// iOS requires to end refreshing before unmounting.
[_refreshControl endRefreshing];
if (@available(macCatalyst 13.1, *)) {
_scrollViewComponentView.scrollView.refreshControl = nil;
}
_scrollViewComponentView = nil;
}
#pragma mark - Native commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTPullToRefreshViewHandleCommand(self, commandName, args);
}
- (void)setNativeRefreshing:(BOOL)refreshing
{
if (refreshing) {
[_refreshControl beginRefreshing];
} else {
[_refreshControl endRefreshing];
}
}
#pragma mark - RCTRefreshableProtocol
- (void)setRefreshing:(BOOL)refreshing
{
[self setNativeRefreshing:refreshing];
}
#pragma mark -
- (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN
{
return @"RefreshControl";
}
@end
Class<RCTComponentViewProtocol> RCTPullToRefreshViewCls(void)
{
return RCTPullToRefreshViewComponentView.class;
}

View File

@@ -0,0 +1,62 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTDefines.h>
#import <React/RCTGenericDelegateSplitter.h>
#import <React/RCTMountingTransactionObserving.h>
#import <React/RCTScrollableProtocol.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/*
* UIView class for <ScrollView> component.
*
* By design, the class does not implement any logic that contradicts to the normal behavior of UIScrollView and does
* not contain any special/custom support for things like floating headers, pull-to-refresh components,
* keyboard-avoiding functionality and so on. All that complexity must be implemented inside those components in order
* to keep the complexity of this component manageable.
*/
@interface RCTScrollViewComponentView : RCTViewComponentView <RCTMountingTransactionObserving>
/*
* Finds and returns the closet RCTScrollViewComponentView component to the given view
*/
+ (nullable RCTScrollViewComponentView *)findScrollViewComponentViewForView:(UIView *)view;
/*
* Returns an actual UIScrollView that this component uses under the hood.
*/
@property (nonatomic, strong, readonly) UIScrollView *scrollView;
/*
* Returns the subview of the scroll view that the component uses to mount all subcomponents into. That's useful to
* separate component views from auxiliary views to be able to reliably implement pull-to-refresh- and RTL-related
* functionality.
*/
@property (nonatomic, strong, readonly) UIView *containerView;
/*
* Returns a delegate splitter that can be used to subscribe for UIScrollView delegate.
*/
@property (nonatomic, strong, readonly)
RCTGenericDelegateSplitter<id<UIScrollViewDelegate>> *scrollViewDelegateSplitter;
@end
/*
* RCTScrollableProtocol is a protocol which RCTScrollViewManager uses to communicate with all kinds of `UIScrollView`s.
* Until Fabric has own command-execution pipeline we have to support that to some extent. The implementation shouldn't
* be perfect though because very soon we will migrate that to the new commands infra and get rid of this.
*/
@interface RCTScrollViewComponentView (ScrollableProtocol) <RCTScrollableProtocol>
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,816 @@
/*
* 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 "RCTScrollViewComponentView.h"
#import <React/RCTAssert.h>
#import <React/RCTBridge+Private.h>
#import <React/RCTConstants.h>
#import <React/RCTScrollEvent.h>
#import <react/renderer/components/scrollview/RCTComponentViewHelpers.h>
#import <react/renderer/components/scrollview/ScrollViewComponentDescriptor.h>
#import <react/renderer/components/scrollview/ScrollViewEventEmitter.h>
#import <react/renderer/components/scrollview/ScrollViewProps.h>
#import <react/renderer/components/scrollview/ScrollViewState.h>
#import <react/renderer/components/scrollview/conversions.h>
#import "RCTConversions.h"
#import "RCTCustomPullToRefreshViewProtocol.h"
#import "RCTEnhancedScrollView.h"
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
static const CGFloat kClippingLeeway = 44.0;
static UIScrollViewKeyboardDismissMode RCTUIKeyboardDismissModeFromProps(const ScrollViewProps &props)
{
switch (props.keyboardDismissMode) {
case ScrollViewKeyboardDismissMode::None:
return UIScrollViewKeyboardDismissModeNone;
case ScrollViewKeyboardDismissMode::OnDrag:
return UIScrollViewKeyboardDismissModeOnDrag;
case ScrollViewKeyboardDismissMode::Interactive:
return UIScrollViewKeyboardDismissModeInteractive;
}
}
static UIScrollViewIndicatorStyle RCTUIScrollViewIndicatorStyleFromProps(const ScrollViewProps &props)
{
switch (props.indicatorStyle) {
case ScrollViewIndicatorStyle::Default:
return UIScrollViewIndicatorStyleDefault;
case ScrollViewIndicatorStyle::Black:
return UIScrollViewIndicatorStyleBlack;
case ScrollViewIndicatorStyle::White:
return UIScrollViewIndicatorStyleWhite;
}
}
// Once Fabric implements proper NativeAnimationDriver, this should be removed.
// This is just a workaround to allow animations based on onScroll event.
// This is only used to animate sticky headers in ScrollViews, and only the contentOffset and tag is used.
// TODO: T116850910 [Fabric][iOS] Make Fabric not use legacy RCTEventDispatcher for native-driven AnimatedEvents
static void RCTSendScrollEventForNativeAnimations_DEPRECATED(UIScrollView *scrollView, NSInteger tag)
{
static uint16_t coalescingKey = 0;
RCTScrollEvent *scrollEvent = [[RCTScrollEvent alloc] initWithEventName:@"onScroll"
reactTag:[NSNumber numberWithInt:tag]
scrollViewContentOffset:scrollView.contentOffset
scrollViewContentInset:scrollView.contentInset
scrollViewContentSize:scrollView.contentSize
scrollViewFrame:scrollView.frame
scrollViewZoomScale:scrollView.zoomScale
userData:nil
coalescingKey:coalescingKey];
RCTBridge *bridge = [RCTBridge currentBridge];
if (bridge) {
[bridge.eventDispatcher sendEvent:scrollEvent];
} else {
NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:scrollEvent, @"event", nil];
[[NSNotificationCenter defaultCenter] postNotificationName:@"RCTNotifyEventDispatcherObserversOfEvent_DEPRECATED"
object:nil
userInfo:userInfo];
}
}
@interface RCTScrollViewComponentView () <
UIScrollViewDelegate,
RCTScrollViewProtocol,
RCTScrollableProtocol,
RCTEnhancedScrollViewOverridingDelegate>
@end
@implementation RCTScrollViewComponentView {
ScrollViewShadowNode::ConcreteState::Shared _state;
CGSize _contentSize;
NSTimeInterval _lastScrollEventDispatchTime;
NSTimeInterval _scrollEventThrottle;
// Flag indicating whether the scrolling that is currently happening
// is triggered by user or not.
// This helps to only update state from `scrollViewDidScroll` in case
// some other part of the system scrolls scroll view.
BOOL _isUserTriggeredScrolling;
BOOL _shouldUpdateContentInsetAdjustmentBehavior;
CGPoint _contentOffsetWhenClipped;
__weak UIView *_contentView;
CGRect _prevFirstVisibleFrame;
__weak UIView *_firstVisibleView;
CGFloat _endDraggingSensitivityMultiplier;
CGFloat _endDraggingSensitivityVelocityMultiplier;
}
+ (RCTScrollViewComponentView *_Nullable)findScrollViewComponentViewForView:(UIView *)view
{
do {
view = view.superview;
} while (view != nil && ![view isKindOfClass:[RCTScrollViewComponentView class]]);
return (RCTScrollViewComponentView *)view;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ScrollViewShadowNode::defaultSharedProps();
_scrollView = [[RCTEnhancedScrollView alloc] initWithFrame:self.bounds];
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_scrollView.delaysContentTouches = NO;
((RCTEnhancedScrollView *)_scrollView).overridingDelegate = self;
_isUserTriggeredScrolling = NO;
_shouldUpdateContentInsetAdjustmentBehavior = YES;
[self addSubview:_scrollView];
_containerView = [[UIView alloc] initWithFrame:CGRectZero];
[_scrollView addSubview:_containerView];
[self.scrollViewDelegateSplitter addDelegate:self];
_scrollEventThrottle = 0;
_endDraggingSensitivityVelocityMultiplier = 0;
_endDraggingSensitivityMultiplier = 1;
}
return self;
}
- (void)dealloc
{
// Removing all delegates from the splitter nils the actual delegate which prevents a crash on UIScrollView
// deallocation.
[self.scrollViewDelegateSplitter removeAllDelegates];
}
- (RCTGenericDelegateSplitter<id<UIScrollViewDelegate>> *)scrollViewDelegateSplitter
{
return ((RCTEnhancedScrollView *)_scrollView).delegateSplitter;
}
#pragma mark - RCTMountingTransactionObserving
- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
[self _prepareForMaintainVisibleScrollPosition];
}
- (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
[self _remountChildren];
[self _adjustForMaintainVisibleContentPosition];
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<ScrollViewComponentDescriptor>();
}
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
if (layoutMetrics.layoutDirection != oldLayoutMetrics.layoutDirection) {
CGAffineTransform transform = (layoutMetrics.layoutDirection == LayoutDirection::LeftToRight)
? CGAffineTransformIdentity
: CGAffineTransformMakeScale(-1, 1);
_containerView.transform = transform;
_scrollView.transform = transform;
}
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldScrollViewProps = static_cast<const ScrollViewProps &>(*_props);
const auto &newScrollViewProps = static_cast<const ScrollViewProps &>(*props);
#define REMAP_PROP(reactName, localName, target) \
if (oldScrollViewProps.reactName != newScrollViewProps.reactName) { \
target.localName = newScrollViewProps.reactName; \
}
#define REMAP_VIEW_PROP(reactName, localName) REMAP_PROP(reactName, localName, self)
#define MAP_VIEW_PROP(name) REMAP_VIEW_PROP(name, name)
#define REMAP_SCROLL_VIEW_PROP(reactName, localName) \
REMAP_PROP(reactName, localName, ((RCTEnhancedScrollView *)_scrollView))
#define MAP_SCROLL_VIEW_PROP(name) REMAP_SCROLL_VIEW_PROP(name, name)
// FIXME: Commented props are not supported yet.
MAP_SCROLL_VIEW_PROP(alwaysBounceHorizontal);
MAP_SCROLL_VIEW_PROP(alwaysBounceVertical);
MAP_SCROLL_VIEW_PROP(bounces);
MAP_SCROLL_VIEW_PROP(bouncesZoom);
MAP_SCROLL_VIEW_PROP(canCancelContentTouches);
MAP_SCROLL_VIEW_PROP(centerContent);
// MAP_SCROLL_VIEW_PROP(automaticallyAdjustContentInsets);
MAP_SCROLL_VIEW_PROP(decelerationRate);
MAP_SCROLL_VIEW_PROP(directionalLockEnabled);
MAP_SCROLL_VIEW_PROP(maximumZoomScale);
MAP_SCROLL_VIEW_PROP(minimumZoomScale);
MAP_SCROLL_VIEW_PROP(scrollEnabled);
MAP_SCROLL_VIEW_PROP(pagingEnabled);
MAP_SCROLL_VIEW_PROP(pinchGestureEnabled);
MAP_SCROLL_VIEW_PROP(scrollsToTop);
MAP_SCROLL_VIEW_PROP(showsHorizontalScrollIndicator);
MAP_SCROLL_VIEW_PROP(showsVerticalScrollIndicator);
if (oldScrollViewProps.scrollIndicatorInsets != newScrollViewProps.scrollIndicatorInsets) {
_scrollView.scrollIndicatorInsets = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.scrollIndicatorInsets);
}
if (oldScrollViewProps.indicatorStyle != newScrollViewProps.indicatorStyle) {
_scrollView.indicatorStyle = RCTUIScrollViewIndicatorStyleFromProps(newScrollViewProps);
}
_endDraggingSensitivityMultiplier = newScrollViewProps.endDraggingSensitivityMultiplier;
_endDraggingSensitivityVelocityMultiplier = newScrollViewProps.endDraggingSensitivityVelocityMultiplier;
if (oldScrollViewProps.scrollEventThrottle != newScrollViewProps.scrollEventThrottle) {
// Zero means "send value only once per significant logical event".
// Prop value is in milliseconds.
// iOS implementation uses `NSTimeInterval` (in seconds).
CGFloat throttleInSeconds = newScrollViewProps.scrollEventThrottle / 1000.0;
CGFloat msPerFrame = 1.0 / 60.0;
if (throttleInSeconds < 0) {
_scrollEventThrottle = INFINITY;
} else if (throttleInSeconds <= msPerFrame) {
_scrollEventThrottle = 0;
} else {
_scrollEventThrottle = throttleInSeconds;
}
}
MAP_SCROLL_VIEW_PROP(zoomScale);
if (oldScrollViewProps.contentInset != newScrollViewProps.contentInset) {
_scrollView.contentInset = RCTUIEdgeInsetsFromEdgeInsets(newScrollViewProps.contentInset);
}
RCTEnhancedScrollView *scrollView = (RCTEnhancedScrollView *)_scrollView;
if (oldScrollViewProps.contentOffset != newScrollViewProps.contentOffset) {
_scrollView.contentOffset = RCTCGPointFromPoint(newScrollViewProps.contentOffset);
}
if (oldScrollViewProps.snapToAlignment != newScrollViewProps.snapToAlignment) {
scrollView.snapToAlignment = RCTNSStringFromString(toString(newScrollViewProps.snapToAlignment));
}
scrollView.snapToStart = newScrollViewProps.snapToStart;
scrollView.snapToEnd = newScrollViewProps.snapToEnd;
if (oldScrollViewProps.snapToOffsets != newScrollViewProps.snapToOffsets) {
NSMutableArray<NSNumber *> *snapToOffsets = [NSMutableArray array];
for (const auto &snapToOffset : newScrollViewProps.snapToOffsets) {
[snapToOffsets addObject:[NSNumber numberWithFloat:snapToOffset]];
}
scrollView.snapToOffsets = snapToOffsets;
}
if (oldScrollViewProps.automaticallyAdjustsScrollIndicatorInsets !=
newScrollViewProps.automaticallyAdjustsScrollIndicatorInsets) {
scrollView.automaticallyAdjustsScrollIndicatorInsets = newScrollViewProps.automaticallyAdjustsScrollIndicatorInsets;
}
if ((oldScrollViewProps.contentInsetAdjustmentBehavior != newScrollViewProps.contentInsetAdjustmentBehavior) ||
_shouldUpdateContentInsetAdjustmentBehavior) {
const auto contentInsetAdjustmentBehavior = newScrollViewProps.contentInsetAdjustmentBehavior;
if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Never) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Automatic) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::ScrollableAxes) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentScrollableAxes;
} else if (contentInsetAdjustmentBehavior == ContentInsetAdjustmentBehavior::Always) {
scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAlways;
}
_shouldUpdateContentInsetAdjustmentBehavior = NO;
}
MAP_SCROLL_VIEW_PROP(disableIntervalMomentum);
MAP_SCROLL_VIEW_PROP(snapToInterval);
if (oldScrollViewProps.keyboardDismissMode != newScrollViewProps.keyboardDismissMode) {
scrollView.keyboardDismissMode = RCTUIKeyboardDismissModeFromProps(newScrollViewProps);
}
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
{
assert(std::dynamic_pointer_cast<ScrollViewShadowNode::ConcreteState const>(state));
_state = std::static_pointer_cast<ScrollViewShadowNode::ConcreteState const>(state);
auto &data = _state->getData();
auto contentOffset = RCTCGPointFromPoint(data.contentOffset);
if (!oldState && !CGPointEqualToPoint(contentOffset, CGPointZero)) {
_scrollView.contentOffset = contentOffset;
}
CGSize contentSize = RCTCGSizeFromSize(data.getContentSize());
if (CGSizeEqualToSize(_contentSize, contentSize)) {
return;
}
_contentSize = contentSize;
_containerView.frame = CGRect{RCTCGPointFromPoint(data.contentBoundingRect.origin), contentSize};
[self _preserveContentOffsetIfNeededWithBlock:^{
self->_scrollView.contentSize = contentSize;
}];
}
/*
* Disables programmatical changing of ScrollView's `contentOffset` if a touch gesture is in progress.
*/
- (void)_preserveContentOffsetIfNeededWithBlock:(void (^)())block
{
if (!block) {
return;
}
if (!_isUserTriggeredScrolling) {
return block();
}
[((RCTEnhancedScrollView *)_scrollView) preserveContentOffsetWithBlock:block];
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[_containerView insertSubview:childComponentView atIndex:index];
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)]) {
_contentView = childComponentView;
}
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[childComponentView removeFromSuperview];
if (![childComponentView conformsToProtocol:@protocol(RCTCustomPullToRefreshViewProtocol)] &&
_contentView == childComponentView) {
_contentView = nil;
}
}
/*
* Returns whether or not the scroll view interaction should be blocked because
* JavaScript was found to be the responder.
*/
- (BOOL)_shouldDisableScrollInteraction
{
UIView *ancestorView = self.superview;
while (ancestorView) {
if ([ancestorView respondsToSelector:@selector(isJSResponder)]) {
BOOL isJSResponder = ((UIView<RCTComponentViewProtocol> *)ancestorView).isJSResponder;
if (isJSResponder) {
return YES;
}
}
ancestorView = ancestorView.superview;
}
return NO;
}
- (ScrollViewMetrics)_scrollViewMetrics
{
ScrollViewMetrics metrics;
metrics.contentSize = RCTSizeFromCGSize(_scrollView.contentSize);
metrics.contentOffset = RCTPointFromCGPoint(_scrollView.contentOffset);
metrics.contentInset = RCTEdgeInsetsFromUIEdgeInsets(_scrollView.contentInset);
metrics.containerSize = RCTSizeFromCGSize(_scrollView.bounds.size);
metrics.zoomScale = _scrollView.zoomScale;
if (_layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
metrics.contentOffset.x = metrics.contentSize.width - metrics.containerSize.width - metrics.contentOffset.x;
}
return metrics;
}
- (void)_updateStateWithContentOffset
{
if (!_state) {
return;
}
auto contentOffset = RCTPointFromCGPoint(_scrollView.contentOffset);
_state->updateState([contentOffset](const ScrollViewShadowNode::ConcreteState::Data &data) {
auto newData = data;
newData.contentOffset = contentOffset;
return std::make_shared<ScrollViewShadowNode::ConcreteState::Data const>(newData);
});
}
- (void)prepareForRecycle
{
const auto &props = static_cast<const ScrollViewProps &>(*_props);
_scrollView.contentOffset = RCTCGPointFromPoint(props.contentOffset);
// We set the default behavior to "never" so that iOS
// doesn't do weird things to UIScrollView insets automatically
// and keeps it as an opt-in behavior.
_scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
_shouldUpdateContentInsetAdjustmentBehavior = YES;
_state.reset();
_isUserTriggeredScrolling = NO;
CGRect oldFrame = self.frame;
self.frame = CGRectZero;
self.frame = oldFrame;
_contentView = nil;
_prevFirstVisibleFrame = CGRectZero;
_firstVisibleView = nil;
[super prepareForRecycle];
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
if (fabs(_endDraggingSensitivityMultiplier - 1) > 0.0001f ||
fabs(_endDraggingSensitivityVelocityMultiplier) > 0.0001f) {
if (targetContentOffset->y > 0) {
const CGFloat travel = targetContentOffset->y - scrollView.contentOffset.y;
targetContentOffset->y = scrollView.contentOffset.y + travel * _endDraggingSensitivityMultiplier +
velocity.y * _endDraggingSensitivityVelocityMultiplier;
}
}
}
- (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
{
// Historically, `UIScrollView`s in React Native do not cancel touches
// started on `UIControl`-based views (as normal iOS `UIScrollView`s do).
return ![self _shouldDisableScrollInteraction];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (!_isUserTriggeredScrolling || CoreFeatures::enableGranularScrollViewStateUpdatesIOS) {
[self _updateStateWithContentOffset];
}
NSTimeInterval now = CACurrentMediaTime();
if ((_lastScrollEventDispatchTime == 0) || (now - _lastScrollEventDispatchTime > _scrollEventThrottle)) {
_lastScrollEventDispatchTime = now;
if (_eventEmitter) {
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onScroll([self _scrollViewMetrics]);
}
RCTSendScrollEventForNativeAnimations_DEPRECATED(scrollView, self.tag);
}
[self _remountChildrenIfNeeded];
}
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
{
[self scrollViewDidScroll:scrollView];
}
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
_isUserTriggeredScrolling = YES;
return YES;
}
- (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView
{
if (!_eventEmitter) {
return;
}
_isUserTriggeredScrolling = NO;
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onScrollToTop([self _scrollViewMetrics]);
[self _updateStateWithContentOffset];
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
[self _forceDispatchNextScrollEvent];
if (!_eventEmitter) {
return;
}
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onScrollBeginDrag([self _scrollViewMetrics]);
_isUserTriggeredScrolling = YES;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
[self _forceDispatchNextScrollEvent];
if (!_eventEmitter) {
return;
}
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onScrollEndDrag([self _scrollViewMetrics]);
[self _updateStateWithContentOffset];
if (!decelerate) {
// ScrollView will not decelerate and `scrollViewDidEndDecelerating` will not be called.
// `_isUserTriggeredScrolling` must be set to NO here.
_isUserTriggeredScrolling = NO;
}
}
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
[self _forceDispatchNextScrollEvent];
if (!_eventEmitter) {
return;
}
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onMomentumScrollBegin([self _scrollViewMetrics]);
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self _forceDispatchNextScrollEvent];
if (!_eventEmitter) {
return;
}
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onMomentumScrollEnd([self _scrollViewMetrics]);
[self _updateStateWithContentOffset];
_isUserTriggeredScrolling = NO;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
[self _handleFinishedScrolling:scrollView];
}
- (void)_handleFinishedScrolling:(UIScrollView *)scrollView
{
[self _forceDispatchNextScrollEvent];
[self scrollViewDidScroll:scrollView];
if (!_eventEmitter) {
return;
}
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onMomentumScrollEnd([self _scrollViewMetrics]);
[self _updateStateWithContentOffset];
}
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view
{
[self _forceDispatchNextScrollEvent];
if (!_eventEmitter) {
return;
}
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onScrollBeginDrag([self _scrollViewMetrics]);
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(nullable UIView *)view atScale:(CGFloat)scale
{
[self _forceDispatchNextScrollEvent];
if (!_eventEmitter) {
return;
}
static_cast<const ScrollViewEventEmitter &>(*_eventEmitter).onScrollEndDrag([self _scrollViewMetrics]);
[self _updateStateWithContentOffset];
}
- (UIView *)viewForZoomingInScrollView:(__unused UIScrollView *)scrollView
{
return _containerView;
}
#pragma mark -
- (void)_forceDispatchNextScrollEvent
{
_lastScrollEventDispatchTime = 0;
}
#pragma mark - Native commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTScrollViewHandleCommand(self, commandName, args);
}
- (void)flashScrollIndicators
{
[_scrollView flashScrollIndicators];
}
- (void)scrollTo:(double)x y:(double)y animated:(BOOL)animated
{
CGPoint offset = CGPointMake(x, y);
CGRect maxRect = CGRectMake(
fmin(-_scrollView.contentInset.left, 0),
fmin(-_scrollView.contentInset.top, 0),
fmax(
_scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right +
fmax(_scrollView.contentInset.left, 0),
0.01),
fmax(
_scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom +
fmax(_scrollView.contentInset.top, 0),
0.01)); // Make width and height greater than 0
const auto &props = static_cast<const ScrollViewProps &>(*_props);
if (!CGRectContainsPoint(maxRect, offset) && !props.scrollToOverflowEnabled) {
CGFloat localX = fmax(offset.x, CGRectGetMinX(maxRect));
localX = fmin(localX, CGRectGetMaxX(maxRect));
CGFloat localY = fmax(offset.y, CGRectGetMinY(maxRect));
localY = fmin(localY, CGRectGetMaxY(maxRect));
offset = CGPointMake(localX, localY);
}
[self scrollToOffset:offset animated:animated];
}
- (void)scrollToEnd:(BOOL)animated
{
BOOL isHorizontal = _scrollView.contentSize.width > self.frame.size.width;
CGPoint offset;
if (isHorizontal) {
CGFloat offsetX = _scrollView.contentSize.width - _scrollView.bounds.size.width + _scrollView.contentInset.right;
offset = CGPointMake(fmax(offsetX, 0), 0);
} else {
CGFloat offsetY = _scrollView.contentSize.height - _scrollView.bounds.size.height + _scrollView.contentInset.bottom;
offset = CGPointMake(0, fmax(offsetY, 0));
}
[self scrollToOffset:offset animated:animated];
}
#pragma mark - Child views mounting
- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
{
// Do nothing. ScrollView manages its subview clipping individually in `_remountChildren`.
}
- (void)_remountChildrenIfNeeded
{
CGPoint contentOffset = _scrollView.contentOffset;
if (std::abs(_contentOffsetWhenClipped.x - contentOffset.x) < kClippingLeeway &&
std::abs(_contentOffsetWhenClipped.y - contentOffset.y) < kClippingLeeway) {
return;
}
_contentOffsetWhenClipped = contentOffset;
[self _remountChildren];
}
- (void)_remountChildren
{
[_scrollView updateClippedSubviewsWithClipRect:CGRectInset(_scrollView.bounds, -kClippingLeeway, -kClippingLeeway)
relativeToView:_scrollView];
}
#pragma mark - RCTScrollableProtocol
- (CGSize)contentSize
{
return _contentSize;
}
- (void)scrollToOffset:(CGPoint)offset
{
[self scrollToOffset:offset animated:YES];
}
- (void)scrollToOffset:(CGPoint)offset animated:(BOOL)animated
{
if (_layoutMetrics.layoutDirection == LayoutDirection::RightToLeft) {
// Adjusting offset.x in right to left layout direction.
offset.x = self.contentSize.width - _scrollView.frame.size.width - offset.x;
}
if (CGPointEqualToPoint(_scrollView.contentOffset, offset)) {
return;
}
[self _forceDispatchNextScrollEvent];
[_scrollView setContentOffset:offset animated:animated];
if (!animated) {
// When not animated, the expected workflow in ``scrollViewDidEndScrollingAnimation`` after scrolling is not going
// to get triggered. We will need to manually execute here.
[self _handleFinishedScrolling:_scrollView];
}
}
- (void)zoomToRect:(CGRect)rect animated:(BOOL)animated
{
[_scrollView zoomToRect:rect animated:animated];
}
- (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
{
[self.scrollViewDelegateSplitter addDelegate:scrollListener];
}
- (void)removeScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
{
[self.scrollViewDelegateSplitter removeDelegate:scrollListener];
}
#pragma mark - Maintain visible content position
- (void)_prepareForMaintainVisibleScrollPosition
{
const auto &props = static_cast<const ScrollViewProps &>(*_props);
if (!props.maintainVisibleContentPosition) {
return;
}
BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
int minIdx = props.maintainVisibleContentPosition.value().minIndexForVisible;
for (NSUInteger ii = minIdx; ii < _contentView.subviews.count; ++ii) {
// Find the first entirely visible view.
UIView *subview = _contentView.subviews[ii];
BOOL hasNewView = NO;
if (horizontal) {
hasNewView = subview.frame.origin.x > _scrollView.contentOffset.x;
} else {
hasNewView = subview.frame.origin.y > _scrollView.contentOffset.y;
}
if (hasNewView || ii == _contentView.subviews.count - 1) {
_prevFirstVisibleFrame = subview.frame;
_firstVisibleView = subview;
break;
}
}
}
- (void)_adjustForMaintainVisibleContentPosition
{
const auto &props = static_cast<const ScrollViewProps &>(*_props);
if (!props.maintainVisibleContentPosition) {
return;
}
std::optional<int> autoscrollThreshold = props.maintainVisibleContentPosition.value().autoscrollToTopThreshold;
BOOL horizontal = _scrollView.contentSize.width > self.frame.size.width;
// TODO: detect and handle/ignore re-ordering
if (horizontal) {
CGFloat deltaX = _firstVisibleView.frame.origin.x - _prevFirstVisibleFrame.origin.x;
if (ABS(deltaX) > 0.5) {
CGFloat x = _scrollView.contentOffset.x;
[self _forceDispatchNextScrollEvent];
_scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x + deltaX, _scrollView.contentOffset.y);
if (autoscrollThreshold) {
// If the offset WAS within the threshold of the start, animate to the start.
if (x <= autoscrollThreshold.value()) {
[self scrollToOffset:CGPointMake(0, _scrollView.contentOffset.y) animated:YES];
}
}
}
} else {
CGRect newFrame = _firstVisibleView.frame;
CGFloat deltaY = newFrame.origin.y - _prevFirstVisibleFrame.origin.y;
if (ABS(deltaY) > 0.5) {
CGFloat y = _scrollView.contentOffset.y;
[self _forceDispatchNextScrollEvent];
_scrollView.contentOffset = CGPointMake(_scrollView.contentOffset.x, _scrollView.contentOffset.y + deltaY);
if (autoscrollThreshold) {
// If the offset WAS within the threshold of the start, animate to the start.
if (y <= autoscrollThreshold.value()) {
[self scrollToOffset:CGPointMake(_scrollView.contentOffset.x, 0) animated:YES];
}
}
}
}
}
@end
Class<RCTComponentViewProtocol> RCTScrollViewCls(void)
{
return RCTScrollViewComponentView.class;
}

View File

@@ -0,0 +1,21 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/**
* UIView class for root <Switch> component.
*/
@interface RCTSwitchComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,120 @@
/*
* 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 "RCTSwitchComponentView.h"
#import <React/RCTConversions.h>
#import <react/renderer/components/rncore/ComponentDescriptors.h>
#import <react/renderer/components/rncore/EventEmitters.h>
#import <react/renderer/components/rncore/Props.h>
#import <react/renderer/components/rncore/RCTComponentViewHelpers.h>
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@interface RCTSwitchComponentView () <RCTSwitchViewProtocol>
@end
@implementation RCTSwitchComponentView {
UISwitch *_switchView;
BOOL _isInitialValueSet;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = SwitchShadowNode::defaultSharedProps();
_switchView = [[UISwitch alloc] initWithFrame:self.bounds];
[_switchView addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
self.contentView = _switchView;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
- (void)prepareForRecycle
{
[super prepareForRecycle];
_isInitialValueSet = NO;
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<SwitchComponentDescriptor>();
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldSwitchProps = static_cast<const SwitchProps &>(*_props);
const auto &newSwitchProps = static_cast<const SwitchProps &>(*props);
// `value`
if (oldSwitchProps.value != newSwitchProps.value) {
BOOL shouldAnimate = _isInitialValueSet == YES;
[_switchView setOn:newSwitchProps.value animated:shouldAnimate];
_isInitialValueSet = YES;
}
// `disabled`
if (oldSwitchProps.disabled != newSwitchProps.disabled) {
_switchView.enabled = !newSwitchProps.disabled;
}
// `tintColor`
if (oldSwitchProps.tintColor != newSwitchProps.tintColor) {
_switchView.tintColor = RCTUIColorFromSharedColor(newSwitchProps.tintColor);
}
// `onTintColor
if (oldSwitchProps.onTintColor != newSwitchProps.onTintColor) {
_switchView.onTintColor = RCTUIColorFromSharedColor(newSwitchProps.onTintColor);
}
// `thumbTintColor`
if (oldSwitchProps.thumbTintColor != newSwitchProps.thumbTintColor) {
_switchView.thumbTintColor = RCTUIColorFromSharedColor(newSwitchProps.thumbTintColor);
}
[super updateProps:props oldProps:oldProps];
}
- (void)onChange:(UISwitch *)sender
{
const auto &props = static_cast<const SwitchProps &>(*_props);
if (props.value == sender.on) {
return;
}
static_cast<const SwitchEventEmitter &>(*_eventEmitter)
.onChange(SwitchEventEmitter::OnChange{.value = static_cast<bool>(sender.on)});
}
#pragma mark - Native Commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTSwitchHandleCommand(self, commandName, args);
}
- (void)setValue:(BOOL)value
{
[_switchView setOn:value animated:YES];
}
@end
Class<RCTComponentViewProtocol> RCTSwitchCls(void)
{
return RCTSwitchComponentView.class;
}

View File

@@ -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 <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTAccessibilityElement : UIAccessibilityElement
/*
* Frame of the accessibility element in parent coordinate system.
* Set to `CGRectZero` to use size of the container.
*
* Default value: `CGRectZero`.
*/
@property (nonatomic, assign) CGRect frame;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,22 @@
/*
* 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 "RCTAccessibilityElement.h"
@implementation RCTAccessibilityElement
- (CGRect)accessibilityFrame
{
UIView *container = (UIView *)self.accessibilityContainer;
if (CGRectEqualToRect(_frame, CGRectZero)) {
return UIAccessibilityConvertFrameToScreenCoordinates(container.bounds, container);
} else {
return UIAccessibilityConvertFrameToScreenCoordinates(_frame, container);
}
}
@end

View File

@@ -0,0 +1,34 @@
/*
* 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 <UIKit/UIKit.h>
#import <react/renderer/attributedstring/AttributedString.h>
#import <react/renderer/attributedstring/ParagraphAttributes.h>
#import <react/renderer/textlayoutmanager/RCTTextLayoutManager.h>
#import "RCTParagraphComponentView.h"
@interface RCTParagraphComponentAccessibilityProvider : NSObject
- (instancetype)initWithString:(facebook::react::AttributedString)attributedString
layoutManager:(RCTTextLayoutManager *)layoutManager
paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
view:(UIView *)view;
/*
* Returns an array of `UIAccessibilityElement`s to be used for `UIAccessibilityContainer` implementation.
*/
- (NSArray<UIAccessibilityElement *> *)accessibilityElements;
/**
@abstract To make sure the provider is up to date.
*/
- (BOOL)isUpToDate:(facebook::react::AttributedString)currentAttributedString;
@end

View File

@@ -0,0 +1,181 @@
/*
* 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 "RCTParagraphComponentAccessibilityProvider.h"
#import <Foundation/Foundation.h>
#import <react/renderer/components/text/ParagraphProps.h>
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
#import <react/renderer/textlayoutmanager/RCTTextLayoutManager.h>
#import <react/renderer/textlayoutmanager/TextLayoutManager.h>
#import "RCTAccessibilityElement.h"
#import "RCTConversions.h"
#import "RCTFabricComponentsPlugins.h"
#import "RCTLocalizationProvider.h"
using namespace facebook::react;
@implementation RCTParagraphComponentAccessibilityProvider {
NSMutableArray<UIAccessibilityElement *> *_accessibilityElements;
AttributedString _attributedString;
RCTTextLayoutManager *_layoutManager;
ParagraphAttributes _paragraphAttributes;
CGRect _frame;
__weak UIView *_view;
}
- (instancetype)initWithString:(facebook::react::AttributedString)attributedString
layoutManager:(RCTTextLayoutManager *)layoutManager
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
frame:(CGRect)frame
view:(UIView *)view
{
if (self = [super init]) {
_attributedString = attributedString;
_layoutManager = layoutManager;
_paragraphAttributes = paragraphAttributes;
_frame = frame;
_view = view;
}
return self;
}
- (NSArray<UIAccessibilityElement *> *)accessibilityElements
{
if (_accessibilityElements) {
return _accessibilityElements;
}
__block NSInteger numberOfLinks = 0;
__block NSInteger numberOfButtons = 0;
__block NSString *truncatedText;
// build an array of the accessibleElements
NSMutableArray<UIAccessibilityElement *> *elements = [NSMutableArray new];
NSString *accessibilityLabel = _view.accessibilityLabel;
if (accessibilityLabel.length == 0) {
accessibilityLabel = RCTNSStringFromString(_attributedString.getString());
}
// add first element has the text for the whole textview in order to read out the whole text
RCTAccessibilityElement *firstElement =
[[RCTAccessibilityElement alloc] initWithAccessibilityContainer:_view.superview];
firstElement.isAccessibilityElement = YES;
firstElement.accessibilityTraits = _view.accessibilityTraits;
firstElement.accessibilityLabel = accessibilityLabel;
firstElement.accessibilityLanguage = _view.accessibilityLanguage;
firstElement.accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(_view.bounds, _view);
[firstElement setAccessibilityActivationPoint:CGPointMake(
firstElement.accessibilityFrame.origin.x + 1.0,
firstElement.accessibilityFrame.origin.y + 1.0)];
[elements addObject:firstElement];
// add additional elements for those parts of text with embedded link so VoiceOver could specially recognize links
[_layoutManager getRectWithAttributedString:_attributedString
paragraphAttributes:_paragraphAttributes
enumerateAttribute:RCTTextAttributesAccessibilityRoleAttributeName
frame:_frame
usingBlock:^(CGRect fragmentRect, NSString *_Nonnull fragmentText, NSString *value) {
if ([fragmentText isEqualToString:firstElement.accessibilityLabel]) {
// The fragment is the entire paragraph. This is handled as `firstElement`.
return;
}
if ((![value isEqualToString:@"button"] && ![value isEqualToString:@"link"])) {
return;
}
if ([value isEqualToString:@"button"] &&
([fragmentText isEqualToString:@"See Less"] ||
[fragmentText isEqualToString:@"See More"])) {
truncatedText = fragmentText;
return;
}
RCTAccessibilityElement *element =
[[RCTAccessibilityElement alloc] initWithAccessibilityContainer:self->_view];
element.isAccessibilityElement = YES;
if ([value isEqualToString:@"link"]) {
element.accessibilityTraits = UIAccessibilityTraitLink;
numberOfLinks++;
} else if ([value isEqualToString:@"button"]) {
element.accessibilityTraits = UIAccessibilityTraitButton;
numberOfButtons++;
}
element.accessibilityLabel = fragmentText;
element.frame = fragmentRect;
[elements addObject:element];
}];
if (numberOfLinks > 0 || numberOfButtons > 0) {
__block NSInteger indexOfLink = 1;
__block NSInteger indexOfButton = 1;
[elements enumerateObjectsUsingBlock:^(UIAccessibilityElement *element, NSUInteger idx, BOOL *_Nonnull stop) {
if (idx == 0) {
return;
}
if (element.accessibilityTraits & UIAccessibilityTraitLink) {
NSString *test = [RCTLocalizationProvider RCTLocalizedString:@"Link %ld of %ld."
withDescription:@"index of the link"];
element.accessibilityHint = [NSString stringWithFormat:test, (long)indexOfLink++, (long)numberOfLinks];
} else {
element.accessibilityHint =
[NSString stringWithFormat:[RCTLocalizationProvider RCTLocalizedString:@"Button %ld of %ld."
withDescription:@"index of the button"],
(long)indexOfButton++,
(long)numberOfButtons];
}
}];
}
if (numberOfLinks > 0 && numberOfButtons > 0) {
firstElement.accessibilityHint =
[RCTLocalizationProvider RCTLocalizedString:@"Links and buttons are found, swipe to move to them."
withDescription:@"accessibility hint for links and buttons inside text"];
} else if (numberOfLinks > 0) {
NSString *firstElementHint = (numberOfLinks == 1)
? [RCTLocalizationProvider RCTLocalizedString:@"One link found, swipe to move to the link."
withDescription:@"accessibility hint for one link inside text"]
: [NSString stringWithFormat:[RCTLocalizationProvider
RCTLocalizedString:@"%ld links found, swipe to move to the first link."
withDescription:@"accessibility hint for multiple links inside text"],
(long)numberOfLinks];
firstElement.accessibilityHint = firstElementHint;
} else if (numberOfButtons > 0) {
NSString *firstElementHint = (numberOfButtons == 1)
? [RCTLocalizationProvider RCTLocalizedString:@"One button found, swipe to move to the button."
withDescription:@"accessibility hint for one button inside text"]
: [NSString stringWithFormat:[RCTLocalizationProvider
RCTLocalizedString:@"%ld buttons found, swipe to move to the first button."
withDescription:@"accessibility hint for multiple buttons inside text"],
(long)numberOfButtons];
firstElement.accessibilityHint = firstElementHint;
}
if (truncatedText && truncatedText.length > 0) {
firstElement.accessibilityHint = (numberOfLinks > 0 || numberOfButtons > 0)
? [NSString
stringWithFormat:@"%@ %@",
firstElement.accessibilityHint,
[RCTLocalizationProvider
RCTLocalizedString:[NSString stringWithFormat:@"Double tap to %@.", truncatedText]
withDescription:@"accessibility hint for truncated text with links or buttons"]]
: [RCTLocalizationProvider RCTLocalizedString:[NSString stringWithFormat:@"Double tap to %@.", truncatedText]
withDescription:@"accessibility hint for truncated text"];
}
// add accessible element for truncation attributed string for automation purposes only
_accessibilityElements = elements;
return _accessibilityElements;
}
- (BOOL)isUpToDate:(facebook::react::AttributedString)currentAttributedString
{
return currentAttributedString == _attributedString;
}
@end

View File

@@ -0,0 +1,27 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/*
* UIView class for <Paragraph> component.
*/
@interface RCTParagraphComponentView : RCTViewComponentView
/*
* Returns an `NSAttributedString` representing the content of the component.
* To be only used by external introspection and debug tools.
*/
@property (nonatomic, nullable, readonly) NSAttributedString *attributedText;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,298 @@
/*
* 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 "RCTParagraphComponentView.h"
#import "RCTParagraphComponentAccessibilityProvider.h"
#import <MobileCoreServices/UTCoreTypes.h>
#import <react/renderer/components/text/ParagraphComponentDescriptor.h>
#import <react/renderer/components/text/ParagraphProps.h>
#import <react/renderer/components/text/ParagraphState.h>
#import <react/renderer/components/text/RawTextComponentDescriptor.h>
#import <react/renderer/components/text/TextComponentDescriptor.h>
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
#import <react/renderer/textlayoutmanager/RCTTextLayoutManager.h>
#import <react/renderer/textlayoutmanager/TextLayoutManager.h>
#import <react/utils/ManagedObjectWrapper.h>
#import "RCTConversions.h"
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@interface RCTParagraphComponentView () <UIEditMenuInteractionDelegate>
@property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0));
@end
@implementation RCTParagraphComponentView {
ParagraphShadowNode::ConcreteState::Shared _state;
ParagraphAttributes _paragraphAttributes;
RCTParagraphComponentAccessibilityProvider *_accessibilityProvider;
UILongPressGestureRecognizer *_longPressGestureRecognizer;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ParagraphShadowNode::defaultSharedProps();
self.opaque = NO;
self.contentMode = UIViewContentModeRedraw;
}
return self;
}
- (NSString *)description
{
NSString *superDescription = [super description];
// Cutting the last `>` character.
if (superDescription.length > 0 && [superDescription characterAtIndex:superDescription.length - 1] == '>') {
superDescription = [superDescription substringToIndex:superDescription.length - 1];
}
return [NSString stringWithFormat:@"%@; attributedText = %@>", superDescription, self.attributedText];
}
- (NSAttributedString *_Nullable)attributedText
{
if (!_state) {
return nil;
}
return RCTNSAttributedStringFromAttributedString(_state->getData().attributedString);
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<ParagraphComponentDescriptor>();
}
+ (std::vector<facebook::react::ComponentDescriptorProvider>)supplementalComponentDescriptorProviders
{
return {
concreteComponentDescriptorProvider<RawTextComponentDescriptor>(),
concreteComponentDescriptorProvider<TextComponentDescriptor>()};
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldParagraphProps = static_cast<const ParagraphProps &>(*_props);
const auto &newParagraphProps = static_cast<const ParagraphProps &>(*props);
_paragraphAttributes = newParagraphProps.paragraphAttributes;
if (newParagraphProps.isSelectable != oldParagraphProps.isSelectable) {
if (newParagraphProps.isSelectable) {
[self enableContextMenu];
} else {
[self disableContextMenu];
}
}
[super updateProps:props oldProps:oldProps];
}
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
{
_state = std::static_pointer_cast<ParagraphShadowNode::ConcreteState const>(state);
[self setNeedsDisplay];
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_state.reset();
_accessibilityProvider = nil;
}
- (void)drawRect:(CGRect)rect
{
if (!_state) {
return;
}
auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager();
auto nsTextStorage = _state->getData().paragraphLayoutManager.getHostTextStorage();
RCTTextLayoutManager *nativeTextLayoutManager =
(RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager());
CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
[nativeTextLayoutManager drawAttributedString:_state->getData().attributedString
paragraphAttributes:_paragraphAttributes
frame:frame
textStorage:unwrapManagedObject(nsTextStorage)];
}
#pragma mark - Accessibility
- (NSString *)accessibilityLabel
{
return self.attributedText.string;
}
- (BOOL)isAccessibilityElement
{
// All accessibility functionality of the component is implemented in `accessibilityElements` method below.
// Hence to avoid calling all other methods from `UIAccessibilityContainer` protocol (most of them have default
// implementations), we return here `NO`.
return NO;
}
- (NSArray *)accessibilityElements
{
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
// If the component is not `accessible`, we return an empty array.
// We do this because logically all nested <Text> components represent the content of the <Paragraph> component;
// in other words, all nested <Text> components individually have no sense without the <Paragraph>.
if (!_state || !paragraphProps.accessible) {
return [NSArray new];
}
auto &data = _state->getData();
if (![_accessibilityProvider isUpToDate:data.attributedString]) {
auto textLayoutManager = data.paragraphLayoutManager.getTextLayoutManager();
RCTTextLayoutManager *nativeTextLayoutManager =
(RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager());
CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
_accessibilityProvider = [[RCTParagraphComponentAccessibilityProvider alloc] initWithString:data.attributedString
layoutManager:nativeTextLayoutManager
paragraphAttributes:data.paragraphAttributes
frame:frame
view:self];
}
return _accessibilityProvider.accessibilityElements;
}
- (UIAccessibilityTraits)accessibilityTraits
{
return [super accessibilityTraits] | UIAccessibilityTraitStaticText;
}
#pragma mark - RCTTouchableComponentViewProtocol
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
{
if (!_state) {
return _eventEmitter;
}
auto textLayoutManager = _state->getData().paragraphLayoutManager.getTextLayoutManager();
RCTTextLayoutManager *nativeTextLayoutManager =
(RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager());
CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
auto eventEmitter = [nativeTextLayoutManager getEventEmitterWithAttributeString:_state->getData().attributedString
paragraphAttributes:_paragraphAttributes
frame:frame
atPoint:point];
if (!eventEmitter) {
return _eventEmitter;
}
assert(std::dynamic_pointer_cast<const TouchEventEmitter>(eventEmitter));
return std::static_pointer_cast<const TouchEventEmitter>(eventEmitter);
}
#pragma mark - Context Menu
- (void)enableContextMenu
{
_longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self
action:@selector(handleLongPress:)];
if (@available(iOS 16.0, *)) {
_editMenuInteraction = [[UIEditMenuInteraction alloc] initWithDelegate:self];
[self addInteraction:_editMenuInteraction];
}
[self addGestureRecognizer:_longPressGestureRecognizer];
}
- (void)disableContextMenu
{
[self removeGestureRecognizer:_longPressGestureRecognizer];
if (@available(iOS 16.0, *)) {
[self removeInteraction:_editMenuInteraction];
_editMenuInteraction = nil;
}
_longPressGestureRecognizer = nil;
}
- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
{
if (@available(iOS 16.0, macCatalyst 16.0, *)) {
CGPoint location = [gesture locationInView:self];
UIEditMenuConfiguration *config = [UIEditMenuConfiguration configurationWithIdentifier:nil sourcePoint:location];
if (_editMenuInteraction) {
[_editMenuInteraction presentEditMenuWithConfiguration:config];
}
} else {
UIMenuController *menuController = [UIMenuController sharedMenuController];
if (menuController.isMenuVisible) {
return;
}
[menuController showMenuFromView:self rect:self.bounds];
}
}
- (BOOL)canBecomeFirstResponder
{
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
return paragraphProps.isSelectable;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
const auto &paragraphProps = static_cast<const ParagraphProps &>(*_props);
if (paragraphProps.isSelectable && action == @selector(copy:)) {
return YES;
}
return [self.nextResponder canPerformAction:action withSender:sender];
}
- (void)copy:(id)sender
{
NSAttributedString *attributedText = self.attributedText;
NSMutableDictionary *item = [NSMutableDictionary new];
NSData *rtf = [attributedText dataFromRange:NSMakeRange(0, attributedText.length)
documentAttributes:@{NSDocumentTypeDocumentAttribute : NSRTFDTextDocumentType}
error:nil];
if (rtf) {
[item setObject:rtf forKey:(id)kUTTypeFlatRTFD];
}
[item setObject:attributedText.string forKey:(id)kUTTypeUTF8PlainText];
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.items = @[ item ];
}
@end
Class<RCTComponentViewProtocol> RCTParagraphCls(void)
{
return RCTParagraphComponentView.class;
}

View File

@@ -0,0 +1,21 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/**
* UIView class for <TextInput> component.
*/
@interface RCTTextInputComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,674 @@
/*
* 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 "RCTTextInputComponentView.h"
#import <react/renderer/components/iostextinput/TextInputComponentDescriptor.h>
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
#import <react/renderer/textlayoutmanager/TextLayoutManager.h>
#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTUITextField.h>
#import <React/RCTUITextView.h>
#import <React/RCTUtils.h>
#import "RCTConversions.h"
#import "RCTTextInputNativeCommands.h"
#import "RCTTextInputUtils.h"
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@interface RCTTextInputComponentView () <RCTBackedTextInputDelegate, RCTTextInputViewProtocol>
@end
@implementation RCTTextInputComponentView {
TextInputShadowNode::ConcreteState::Shared _state;
UIView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
NSUInteger _mostRecentEventCount;
NSAttributedString *_lastStringStateWasUpdatedWith;
/*
* UIKit uses either UITextField or UITextView as its UIKit element for <TextInput>. UITextField is for single line
* entry, UITextView is for multiline entry. There is a problem with order of events when user types a character. In
* UITextField (single line text entry), typing a character first triggers `onChange` event and then
* onSelectionChange. In UITextView (multi line text entry), typing a character first triggers `onSelectionChange` and
* then onChange. JavaScript depends on `onChange` to be called before `onSelectionChange`. This flag keeps state so
* if UITextView is backing text input view, inside `-[RCTTextInputComponentView textInputDidChangeSelection]` we make
* sure to call `onChange` before `onSelectionChange` and ignore next `-[RCTTextInputComponentView
* textInputDidChange]` call.
*/
BOOL _ignoreNextTextInputCall;
/*
* A flag that when set to true, `_mostRecentEventCount` won't be incremented when `[self _updateState]`
* and delegate methods `textInputDidChange` and `textInputDidChangeSelection` will exit early.
*
* Setting `_backedTextInputView.attributedText` triggers delegate methods `textInputDidChange` and
* `textInputDidChangeSelection` for multiline text input only.
* In multiline text input this is undesirable as we don't want to be sending events for changes that JS triggered.
*/
BOOL _comingFromJS;
BOOL _didMoveToWindow;
}
#pragma mark - UIView overrides
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
const auto &defaultProps = TextInputShadowNode::defaultSharedProps();
_props = defaultProps;
_backedTextInputView = defaultProps->traits.multiline ? [RCTUITextView new] : [RCTUITextField new];
_backedTextInputView.textInputDelegate = self;
_ignoreNextTextInputCall = NO;
_comingFromJS = NO;
_didMoveToWindow = NO;
[self addSubview:_backedTextInputView];
}
return self;
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (self.window && !_didMoveToWindow) {
const auto &props = static_cast<const TextInputProps &>(*_props);
if (props.autoFocus) {
[_backedTextInputView becomeFirstResponder];
}
_didMoveToWindow = YES;
}
[self _restoreTextSelection];
}
#pragma mark - RCTViewComponentView overrides
- (NSObject *)accessibilityElement
{
return _backedTextInputView;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<TextInputComponentDescriptor>();
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldTextInputProps = static_cast<const TextInputProps &>(*_props);
const auto &newTextInputProps = static_cast<const TextInputProps &>(*props);
// Traits:
if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) {
[self _setMultiline:newTextInputProps.traits.multiline];
}
if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
_backedTextInputView.autocapitalizationType =
RCTUITextAutocapitalizationTypeFromAutocapitalizationType(newTextInputProps.traits.autocapitalizationType);
}
if (newTextInputProps.traits.autoCorrect != oldTextInputProps.traits.autoCorrect) {
_backedTextInputView.autocorrectionType =
RCTUITextAutocorrectionTypeFromOptionalBool(newTextInputProps.traits.autoCorrect);
}
if (newTextInputProps.traits.contextMenuHidden != oldTextInputProps.traits.contextMenuHidden) {
_backedTextInputView.contextMenuHidden = newTextInputProps.traits.contextMenuHidden;
}
if (newTextInputProps.traits.editable != oldTextInputProps.traits.editable) {
_backedTextInputView.editable = newTextInputProps.traits.editable;
}
if (newTextInputProps.traits.enablesReturnKeyAutomatically !=
oldTextInputProps.traits.enablesReturnKeyAutomatically) {
_backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically;
}
if (newTextInputProps.traits.keyboardAppearance != oldTextInputProps.traits.keyboardAppearance) {
_backedTextInputView.keyboardAppearance =
RCTUIKeyboardAppearanceFromKeyboardAppearance(newTextInputProps.traits.keyboardAppearance);
}
if (newTextInputProps.traits.spellCheck != oldTextInputProps.traits.spellCheck) {
_backedTextInputView.spellCheckingType =
RCTUITextSpellCheckingTypeFromOptionalBool(newTextInputProps.traits.spellCheck);
}
if (newTextInputProps.traits.caretHidden != oldTextInputProps.traits.caretHidden) {
_backedTextInputView.caretHidden = newTextInputProps.traits.caretHidden;
}
if (newTextInputProps.traits.clearButtonMode != oldTextInputProps.traits.clearButtonMode) {
_backedTextInputView.clearButtonMode =
RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(newTextInputProps.traits.clearButtonMode);
}
if (newTextInputProps.traits.scrollEnabled != oldTextInputProps.traits.scrollEnabled) {
_backedTextInputView.scrollEnabled = newTextInputProps.traits.scrollEnabled;
}
if (newTextInputProps.traits.secureTextEntry != oldTextInputProps.traits.secureTextEntry) {
_backedTextInputView.secureTextEntry = newTextInputProps.traits.secureTextEntry;
}
if (newTextInputProps.traits.keyboardType != oldTextInputProps.traits.keyboardType) {
_backedTextInputView.keyboardType = RCTUIKeyboardTypeFromKeyboardType(newTextInputProps.traits.keyboardType);
}
if (newTextInputProps.traits.returnKeyType != oldTextInputProps.traits.returnKeyType) {
_backedTextInputView.returnKeyType = RCTUIReturnKeyTypeFromReturnKeyType(newTextInputProps.traits.returnKeyType);
}
if (newTextInputProps.traits.textContentType != oldTextInputProps.traits.textContentType) {
_backedTextInputView.textContentType = RCTUITextContentTypeFromString(newTextInputProps.traits.textContentType);
}
if (newTextInputProps.traits.passwordRules != oldTextInputProps.traits.passwordRules) {
_backedTextInputView.passwordRules = RCTUITextInputPasswordRulesFromString(newTextInputProps.traits.passwordRules);
}
if (newTextInputProps.traits.smartInsertDelete != oldTextInputProps.traits.smartInsertDelete) {
_backedTextInputView.smartInsertDeleteType =
RCTUITextSmartInsertDeleteTypeFromOptionalBool(newTextInputProps.traits.smartInsertDelete);
}
// Traits `blurOnSubmit`, `clearTextOnFocus`, and `selectTextOnFocus` were omitted intentionally here
// because they are being checked on-demand.
// Other props:
if (newTextInputProps.placeholder != oldTextInputProps.placeholder) {
_backedTextInputView.placeholder = RCTNSStringFromString(newTextInputProps.placeholder);
}
if (newTextInputProps.placeholderTextColor != oldTextInputProps.placeholderTextColor) {
_backedTextInputView.placeholderColor = RCTUIColorFromSharedColor(newTextInputProps.placeholderTextColor);
}
if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) {
_backedTextInputView.defaultTextAttributes =
RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
}
if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) {
_backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor);
}
if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) {
_backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID);
}
[super updateProps:props oldProps:oldProps];
[self setDefaultInputAccessoryView];
}
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
{
_state = std::static_pointer_cast<TextInputShadowNode::ConcreteState const>(state);
if (!_state) {
assert(false && "State is `null` for <TextInput> component.");
_backedTextInputView.attributedText = nil;
return;
}
auto data = _state->getData();
if (!oldState) {
_mostRecentEventCount = _state->getData().mostRecentEventCount;
}
if (_mostRecentEventCount == _state->getData().mostRecentEventCount) {
_comingFromJS = YES;
[self _setAttributedString:RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox)];
_comingFromJS = NO;
}
}
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
_backedTextInputView.frame =
UIEdgeInsetsInsetRect(self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth));
_backedTextInputView.textContainerInset =
RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.contentInsets - layoutMetrics.borderWidth);
if (_eventEmitter) {
static_cast<const TextInputEventEmitter &>(*_eventEmitter).onContentSizeChange([self _textInputMetrics]);
}
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_state.reset();
_backedTextInputView.attributedText = nil;
_mostRecentEventCount = 0;
_comingFromJS = NO;
_lastStringStateWasUpdatedWith = nil;
_ignoreNextTextInputCall = NO;
_didMoveToWindow = NO;
[_backedTextInputView resignFirstResponder];
}
#pragma mark - RCTBackedTextInputDelegate
- (BOOL)textInputShouldBeginEditing
{
return YES;
}
- (void)textInputDidBeginEditing
{
const auto &props = static_cast<const TextInputProps &>(*_props);
if (props.traits.clearTextOnFocus) {
_backedTextInputView.attributedText = nil;
[self textInputDidChange];
}
if (props.traits.selectTextOnFocus) {
[_backedTextInputView selectAll:nil];
[self textInputDidChangeSelection];
}
if (_eventEmitter) {
static_cast<const TextInputEventEmitter &>(*_eventEmitter).onFocus([self _textInputMetrics]);
}
}
- (BOOL)textInputShouldEndEditing
{
return YES;
}
- (void)textInputDidEndEditing
{
if (_eventEmitter) {
static_cast<const TextInputEventEmitter &>(*_eventEmitter).onEndEditing([self _textInputMetrics]);
static_cast<const TextInputEventEmitter &>(*_eventEmitter).onBlur([self _textInputMetrics]);
}
}
- (BOOL)textInputShouldSubmitOnReturn
{
const SubmitBehavior submitBehavior = [self getSubmitBehavior];
const BOOL shouldSubmit = submitBehavior == SubmitBehavior::Submit || submitBehavior == SubmitBehavior::BlurAndSubmit;
// We send `submit` event here, in `textInputShouldSubmitOnReturn`
// (not in `textInputDidReturn)`, because of semantic of the event:
// `onSubmitEditing` is called when "Submit" button
// (the blue key on onscreen keyboard) did pressed
// (no connection to any specific "submitting" process).
if (_eventEmitter && shouldSubmit) {
static_cast<const TextInputEventEmitter &>(*_eventEmitter).onSubmitEditing([self _textInputMetrics]);
}
return shouldSubmit;
}
- (BOOL)textInputShouldReturn
{
return [self getSubmitBehavior] == SubmitBehavior::BlurAndSubmit;
}
- (void)textInputDidReturn
{
// Does nothing.
}
- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
{
const auto &props = static_cast<const TextInputProps &>(*_props);
if (!_backedTextInputView.textWasPasted) {
if (_eventEmitter) {
KeyPressMetrics keyPressMetrics;
keyPressMetrics.text = RCTStringFromNSString(text);
keyPressMetrics.eventCount = _mostRecentEventCount;
const auto &textInputEventEmitter = static_cast<const TextInputEventEmitter &>(*_eventEmitter);
if (props.onKeyPressSync) {
textInputEventEmitter.onKeyPressSync(keyPressMetrics);
} else {
textInputEventEmitter.onKeyPress(keyPressMetrics);
}
}
}
if (props.maxLength) {
NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length;
if (allowedLength > 0 && text.length > allowedLength) {
// make sure unicode characters that are longer than 16 bits (such as emojis) are not cut off
NSRange cutOffCharacterRange = [text rangeOfComposedCharacterSequenceAtIndex:allowedLength - 1];
if (cutOffCharacterRange.location + cutOffCharacterRange.length > allowedLength) {
// the character at the length limit takes more than 16bits, truncation should end at the character before
allowedLength = cutOffCharacterRange.location;
}
}
if (allowedLength <= 0) {
return nil;
}
return allowedLength > text.length ? text : [text substringToIndex:allowedLength];
}
return text;
}
- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
return YES;
}
- (void)textInputDidChange
{
if (_comingFromJS) {
return;
}
if (_ignoreNextTextInputCall && [_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
_ignoreNextTextInputCall = NO;
return;
}
[self _updateState];
if (_eventEmitter) {
const auto &textInputEventEmitter = static_cast<const TextInputEventEmitter &>(*_eventEmitter);
const auto &props = static_cast<const TextInputProps &>(*_props);
if (props.onChangeSync) {
textInputEventEmitter.onChangeSync([self _textInputMetrics]);
} else {
textInputEventEmitter.onChange([self _textInputMetrics]);
}
}
}
- (void)textInputDidChangeSelection
{
if (_comingFromJS) {
return;
}
const auto &props = static_cast<const TextInputProps &>(*_props);
if (props.traits.multiline && ![_lastStringStateWasUpdatedWith isEqual:_backedTextInputView.attributedText]) {
[self textInputDidChange];
_ignoreNextTextInputCall = YES;
}
if (_eventEmitter) {
static_cast<const TextInputEventEmitter &>(*_eventEmitter).onSelectionChange([self _textInputMetrics]);
}
}
#pragma mark - RCTBackedTextInputDelegate (UIScrollViewDelegate)
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (_eventEmitter) {
static_cast<const TextInputEventEmitter &>(*_eventEmitter).onScroll([self _textInputMetrics]);
}
}
#pragma mark - Native Commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTTextInputHandleCommand(self, commandName, args);
}
- (void)focus
{
[_backedTextInputView becomeFirstResponder];
}
- (void)blur
{
[_backedTextInputView resignFirstResponder];
}
- (void)setTextAndSelection:(NSInteger)eventCount
value:(NSString *__nullable)value
start:(NSInteger)start
end:(NSInteger)end
{
if (_mostRecentEventCount != eventCount) {
return;
}
_comingFromJS = YES;
if (value && ![value isEqualToString:_backedTextInputView.attributedText.string]) {
NSAttributedString *attributedString =
[[NSAttributedString alloc] initWithString:value attributes:_backedTextInputView.defaultTextAttributes];
[self _setAttributedString:attributedString];
[self _updateState];
}
UITextPosition *startPosition = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
offset:start];
UITextPosition *endPosition = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
offset:end];
if (startPosition && endPosition) {
UITextRange *range = [_backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition];
[_backedTextInputView setSelectedTextRange:range notifyDelegate:NO];
}
_comingFromJS = NO;
}
#pragma mark - Default input accessory view
- (void)setDefaultInputAccessoryView
{
// InputAccessoryView component sets the inputAccessoryView when inputAccessoryViewID exists
if (_backedTextInputView.inputAccessoryViewID) {
if (_backedTextInputView.isFirstResponder) {
[_backedTextInputView reloadInputViews];
}
return;
}
UIKeyboardType keyboardType = _backedTextInputView.keyboardType;
// These keyboard types (all are number pads) don't have a "Done" button by default,
// so we create an `inputAccessoryView` with this button for them.
BOOL shouldHaveInputAccessoryView =
(keyboardType == UIKeyboardTypeNumberPad || keyboardType == UIKeyboardTypePhonePad ||
keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad) &&
_backedTextInputView.returnKeyType == UIReturnKeyDone;
if ((_backedTextInputView.inputAccessoryView != nil) == shouldHaveInputAccessoryView) {
return;
}
if (shouldHaveInputAccessoryView) {
UIToolbar *toolbarView = [UIToolbar new];
[toolbarView sizeToFit];
UIBarButtonItem *flexibleSpace =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *doneButton =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(handleInputAccessoryDoneButton)];
toolbarView.items = @[ flexibleSpace, doneButton ];
_backedTextInputView.inputAccessoryView = toolbarView;
} else {
_backedTextInputView.inputAccessoryView = nil;
}
if (_backedTextInputView.isFirstResponder) {
[_backedTextInputView reloadInputViews];
}
}
- (void)handleInputAccessoryDoneButton
{
if ([self textInputShouldReturn]) {
[_backedTextInputView endEditing:YES];
}
}
#pragma mark - Other
- (TextInputMetrics)_textInputMetrics
{
TextInputMetrics metrics;
metrics.text = RCTStringFromNSString(_backedTextInputView.attributedText.string);
metrics.selectionRange = [self _selectionRange];
metrics.eventCount = _mostRecentEventCount;
CGPoint contentOffset = _backedTextInputView.contentOffset;
metrics.contentOffset = {contentOffset.x, contentOffset.y};
UIEdgeInsets contentInset = _backedTextInputView.contentInset;
metrics.contentInset = {contentInset.left, contentInset.top, contentInset.right, contentInset.bottom};
CGSize contentSize = _backedTextInputView.contentSize;
metrics.contentSize = {contentSize.width, contentSize.height};
CGSize layoutMeasurement = _backedTextInputView.bounds.size;
metrics.layoutMeasurement = {layoutMeasurement.width, layoutMeasurement.height};
CGFloat zoomScale = _backedTextInputView.zoomScale;
metrics.zoomScale = zoomScale;
return metrics;
}
- (void)_updateState
{
if (!_state) {
return;
}
NSAttributedString *attributedString = _backedTextInputView.attributedText;
auto data = _state->getData();
_lastStringStateWasUpdatedWith = attributedString;
data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString);
_mostRecentEventCount += _comingFromJS ? 0 : 1;
data.mostRecentEventCount = _mostRecentEventCount;
_state->updateState(std::move(data));
}
- (AttributedString::Range)_selectionRange
{
UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange;
NSInteger start = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
toPosition:selectedTextRange.start];
NSInteger end = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
toPosition:selectedTextRange.end];
return AttributedString::Range{(int)start, (int)(end - start)};
}
- (void)_restoreTextSelection
{
const auto &selection = static_cast<const TextInputProps &>(*_props).selection;
if (!selection.has_value()) {
return;
}
auto start = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
offset:selection->start];
auto end = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument offset:selection->end];
auto range = [_backedTextInputView textRangeFromPosition:start toPosition:end];
[_backedTextInputView setSelectedTextRange:range notifyDelegate:YES];
}
- (void)_setAttributedString:(NSAttributedString *)attributedString
{
if ([self _textOf:attributedString equals:_backedTextInputView.attributedText]) {
return;
}
UITextRange *selectedRange = _backedTextInputView.selectedTextRange;
NSInteger oldTextLength = _backedTextInputView.attributedText.string.length;
_backedTextInputView.attributedText = attributedString;
if (selectedRange.empty) {
// Maintaining a cursor position relative to the end of the old text.
NSInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
toPosition:selectedRange.start];
NSInteger offsetFromEnd = oldTextLength - offsetStart;
NSInteger newOffset = attributedString.string.length - offsetFromEnd;
UITextPosition *position = [_backedTextInputView positionFromPosition:_backedTextInputView.beginningOfDocument
offset:newOffset];
[_backedTextInputView setSelectedTextRange:[_backedTextInputView textRangeFromPosition:position toPosition:position]
notifyDelegate:YES];
}
[self _restoreTextSelection];
_lastStringStateWasUpdatedWith = attributedString;
}
- (void)_setMultiline:(BOOL)multiline
{
[_backedTextInputView removeFromSuperview];
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new];
backedTextInputView.frame = _backedTextInputView.frame;
RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView);
_backedTextInputView = backedTextInputView;
[self addSubview:_backedTextInputView];
}
- (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText
{
// When the dictation is running we can't update the attributed text on the backed up text view
// because setting the attributed string will kill the dictation. This means that we can't impose
// the settings on a dictation.
// Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the
// text that we should disregard. See
// https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc for more info.
// Also, updating the attributed text while inputting Korean language will break input mechanism.
// If the user added an emoji, the system adds a font attribute for the emoji and stores the original font in
// NSOriginalFont. Lastly, when entering a password, etc., there will be additional styling on the field as the native
// text view handles showing the last character for a split second.
__block BOOL fontHasBeenUpdatedBySystem = false;
[oldText enumerateAttribute:@"NSOriginalFont"
inRange:NSMakeRange(0, oldText.length)
options:0
usingBlock:^(id value, NSRange range, BOOL *stop) {
if (value) {
fontHasBeenUpdatedBySystem = true;
}
}];
BOOL shouldFallbackToBareTextComparison =
[_backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] ||
[_backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"ko-KR"] ||
_backedTextInputView.markedTextRange || _backedTextInputView.isSecureTextEntry || fontHasBeenUpdatedBySystem;
if (shouldFallbackToBareTextComparison) {
return ([newText.string isEqualToString:oldText.string]);
} else {
return ([newText isEqualToAttributedString:oldText]);
}
}
- (SubmitBehavior)getSubmitBehavior
{
const auto &props = static_cast<const TextInputProps &>(*_props);
const SubmitBehavior submitBehaviorDefaultable = props.traits.submitBehavior;
// We should always have a non-default `submitBehavior`, but in case we don't, set it based on multiline.
if (submitBehaviorDefaultable == SubmitBehavior::Default) {
return props.traits.multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit;
}
return submitBehaviorDefaultable;
}
@end
Class<RCTComponentViewProtocol> RCTTextInputCls(void)
{
return RCTTextInputComponentView.class;
}

View File

@@ -0,0 +1,104 @@
/*
* 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 <Foundation/Foundation.h>
#import <React/RCTDefines.h>
#import <React/RCTLog.h>
NS_ASSUME_NONNULL_BEGIN
@protocol RCTTextInputViewProtocol <NSObject>
- (void)focus;
- (void)blur;
- (void)setTextAndSelection:(NSInteger)eventCount
value:(NSString *__nullable)value
start:(NSInteger)start
end:(NSInteger)end;
@end
RCT_EXTERN inline void
RCTTextInputHandleCommand(id<RCTTextInputViewProtocol> componentView, const NSString *commandName, const NSArray *args)
{
if ([commandName isEqualToString:@"focus"]) {
#if RCT_DEBUG
if ([args count] != 0) {
RCTLogError(
@"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0);
return;
}
#endif
[componentView focus];
return;
}
if ([commandName isEqualToString:@"blur"]) {
#if RCT_DEBUG
if ([args count] != 0) {
RCTLogError(
@"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 0);
return;
}
#endif
[componentView blur];
return;
}
if ([commandName isEqualToString:@"setTextAndSelection"]) {
#if RCT_DEBUG
if ([args count] != 4) {
RCTLogError(
@"%@ command %@ received %d arguments, expected %d.", @"TextInput", commandName, (int)[args count], 4);
return;
}
#endif
NSObject *arg0 = args[0];
#if RCT_DEBUG
if (!RCTValidateTypeOfViewCommandArgument(arg0, [NSNumber class], @"number", @"TextInput", commandName, @"1st")) {
return;
}
#endif
NSInteger eventCount = [(NSNumber *)arg0 intValue];
NSObject *arg1 = args[1];
#if RCT_DEBUG
if (![arg1 isKindOfClass:[NSNull class]] &&
!RCTValidateTypeOfViewCommandArgument(arg1, [NSString class], @"string", @"TextInput", commandName, @"2nd")) {
return;
}
#endif
NSString *value = [arg1 isKindOfClass:[NSNull class]] ? nil : (NSString *)arg1;
NSObject *arg2 = args[2];
#if RCT_DEBUG
if (!RCTValidateTypeOfViewCommandArgument(arg2, [NSNumber class], @"number", @"TextInput", commandName, @"3rd")) {
return;
}
#endif
NSInteger start = [(NSNumber *)arg2 intValue];
NSObject *arg3 = args[3];
#if RCT_DEBUG
if (!RCTValidateTypeOfViewCommandArgument(arg3, [NSNumber class], @"number", @"TextInput", commandName, @"4th")) {
return;
}
#endif
NSInteger end = [(NSNumber *)arg3 intValue];
[componentView setTextAndSelection:eventCount value:value start:start end:end];
return;
}
#if RCT_DEBUG
RCTLogError(@"%@ received command %@, which is not a supported command.", @"TextInput", commandName);
#endif
}
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,44 @@
/*
* 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 <UIKit/UIKit.h>
#import <optional>
#import <React/RCTBackedTextInputViewProtocol.h>
#import <react/renderer/components/iostextinput/primitives.h>
NS_ASSUME_NONNULL_BEGIN
void RCTCopyBackedTextInput(
UIView<RCTBackedTextInputViewProtocol> *fromTextInput,
UIView<RCTBackedTextInputViewProtocol> *toTextInput);
UITextAutocorrectionType RCTUITextAutocorrectionTypeFromOptionalBool(std::optional<bool> autoCorrect);
UITextAutocapitalizationType RCTUITextAutocapitalizationTypeFromAutocapitalizationType(
facebook::react::AutocapitalizationType autocapitalizationType);
UIKeyboardAppearance RCTUIKeyboardAppearanceFromKeyboardAppearance(
facebook::react::KeyboardAppearance keyboardAppearance);
UITextSpellCheckingType RCTUITextSpellCheckingTypeFromOptionalBool(std::optional<bool> spellCheck);
UITextFieldViewMode RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(
facebook::react::TextInputAccessoryVisibilityMode mode);
UIKeyboardType RCTUIKeyboardTypeFromKeyboardType(facebook::react::KeyboardType keyboardType);
UIReturnKeyType RCTUIReturnKeyTypeFromReturnKeyType(facebook::react::ReturnKeyType returnKeyType);
UITextContentType RCTUITextContentTypeFromString(const std::string &contentType);
UITextInputPasswordRules *RCTUITextInputPasswordRulesFromString(const std::string &passwordRules);
UITextSmartInsertDeleteType RCTUITextSmartInsertDeleteTypeFromOptionalBool(std::optional<bool> smartInsertDelete);
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,253 @@
/*
* 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 "RCTTextInputUtils.h"
#import <React/RCTConversions.h>
using namespace facebook::react;
static NSAttributedString *RCTSanitizeAttributedString(NSAttributedString *attributedString)
{
// Here we need to remove text attributes specific to particular kind of TextInput on iOS (e.g. limiting line number).
// TODO: Implement it properly.
return [[NSAttributedString alloc] initWithString:attributedString.string];
}
void RCTCopyBackedTextInput(
UIView<RCTBackedTextInputViewProtocol> *fromTextInput,
UIView<RCTBackedTextInputViewProtocol> *toTextInput)
{
toTextInput.attributedText = RCTSanitizeAttributedString(fromTextInput.attributedText);
toTextInput.placeholder = fromTextInput.placeholder;
toTextInput.placeholderColor = fromTextInput.placeholderColor;
toTextInput.textContainerInset = fromTextInput.textContainerInset;
toTextInput.inputAccessoryView = fromTextInput.inputAccessoryView;
toTextInput.textInputDelegate = fromTextInput.textInputDelegate;
toTextInput.placeholderColor = fromTextInput.placeholderColor;
toTextInput.defaultTextAttributes = fromTextInput.defaultTextAttributes;
toTextInput.autocapitalizationType = fromTextInput.autocapitalizationType;
toTextInput.autocorrectionType = fromTextInput.autocorrectionType;
toTextInput.contextMenuHidden = fromTextInput.contextMenuHidden;
toTextInput.editable = fromTextInput.editable;
toTextInput.enablesReturnKeyAutomatically = fromTextInput.enablesReturnKeyAutomatically;
toTextInput.keyboardAppearance = fromTextInput.keyboardAppearance;
toTextInput.spellCheckingType = fromTextInput.spellCheckingType;
toTextInput.caretHidden = fromTextInput.caretHidden;
toTextInput.clearButtonMode = fromTextInput.clearButtonMode;
toTextInput.scrollEnabled = fromTextInput.scrollEnabled;
toTextInput.secureTextEntry = fromTextInput.secureTextEntry;
toTextInput.keyboardType = fromTextInput.keyboardType;
toTextInput.textContentType = fromTextInput.textContentType;
toTextInput.smartInsertDeleteType = fromTextInput.smartInsertDeleteType;
toTextInput.passwordRules = fromTextInput.passwordRules;
[toTextInput setSelectedTextRange:fromTextInput.selectedTextRange notifyDelegate:NO];
}
UITextAutocorrectionType RCTUITextAutocorrectionTypeFromOptionalBool(std::optional<bool> autoCorrect)
{
return autoCorrect.has_value() ? (*autoCorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo)
: UITextAutocorrectionTypeDefault;
}
UITextAutocapitalizationType RCTUITextAutocapitalizationTypeFromAutocapitalizationType(
AutocapitalizationType autocapitalizationType)
{
switch (autocapitalizationType) {
case AutocapitalizationType::None:
return UITextAutocapitalizationTypeNone;
case AutocapitalizationType::Words:
return UITextAutocapitalizationTypeWords;
case AutocapitalizationType::Sentences:
return UITextAutocapitalizationTypeSentences;
case AutocapitalizationType::Characters:
return UITextAutocapitalizationTypeAllCharacters;
}
}
UIKeyboardAppearance RCTUIKeyboardAppearanceFromKeyboardAppearance(KeyboardAppearance keyboardAppearance)
{
switch (keyboardAppearance) {
case KeyboardAppearance::Default:
return UIKeyboardAppearanceDefault;
case KeyboardAppearance::Light:
return UIKeyboardAppearanceLight;
case KeyboardAppearance::Dark:
return UIKeyboardAppearanceDark;
}
}
UITextSpellCheckingType RCTUITextSpellCheckingTypeFromOptionalBool(std::optional<bool> spellCheck)
{
return spellCheck.has_value() ? (*spellCheck ? UITextSpellCheckingTypeYes : UITextSpellCheckingTypeNo)
: UITextSpellCheckingTypeDefault;
}
UITextFieldViewMode RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(
facebook::react::TextInputAccessoryVisibilityMode mode)
{
switch (mode) {
case TextInputAccessoryVisibilityMode::Never:
return UITextFieldViewModeNever;
case TextInputAccessoryVisibilityMode::WhileEditing:
return UITextFieldViewModeWhileEditing;
case TextInputAccessoryVisibilityMode::UnlessEditing:
return UITextFieldViewModeUnlessEditing;
case TextInputAccessoryVisibilityMode::Always:
return UITextFieldViewModeAlways;
}
}
UIKeyboardType RCTUIKeyboardTypeFromKeyboardType(KeyboardType keyboardType)
{
switch (keyboardType) {
// Universal
case KeyboardType::Default:
return UIKeyboardTypeDefault;
case KeyboardType::EmailAddress:
return UIKeyboardTypeEmailAddress;
case KeyboardType::Numeric:
return UIKeyboardTypeDecimalPad;
case KeyboardType::PhonePad:
return UIKeyboardTypePhonePad;
case KeyboardType::NumberPad:
return UIKeyboardTypeNumberPad;
case KeyboardType::DecimalPad:
return UIKeyboardTypeDecimalPad;
// iOS-only
case KeyboardType::ASCIICapable:
return UIKeyboardTypeASCIICapable;
case KeyboardType::NumbersAndPunctuation:
return UIKeyboardTypeNumbersAndPunctuation;
case KeyboardType::URL:
return UIKeyboardTypeURL;
case KeyboardType::NamePhonePad:
return UIKeyboardTypeNamePhonePad;
case KeyboardType::Twitter:
return UIKeyboardTypeTwitter;
case KeyboardType::WebSearch:
return UIKeyboardTypeWebSearch;
case KeyboardType::ASCIICapableNumberPad:
return UIKeyboardTypeASCIICapableNumberPad;
// Android-only
case KeyboardType::VisiblePassword:
return UIKeyboardTypeDefault;
}
}
UIReturnKeyType RCTUIReturnKeyTypeFromReturnKeyType(ReturnKeyType returnKeyType)
{
switch (returnKeyType) {
case ReturnKeyType::Default:
return UIReturnKeyDefault;
case ReturnKeyType::Done:
return UIReturnKeyDone;
case ReturnKeyType::Go:
return UIReturnKeyGo;
case ReturnKeyType::Next:
return UIReturnKeyNext;
case ReturnKeyType::Search:
return UIReturnKeySearch;
case ReturnKeyType::Send:
return UIReturnKeySend;
// iOS-only
case ReturnKeyType::EmergencyCall:
return UIReturnKeyEmergencyCall;
case ReturnKeyType::Google:
return UIReturnKeyGoogle;
case ReturnKeyType::Join:
return UIReturnKeyJoin;
case ReturnKeyType::Route:
return UIReturnKeyRoute;
case ReturnKeyType::Yahoo:
return UIReturnKeyYahoo;
case ReturnKeyType::Continue:
return UIReturnKeyContinue;
// Android-only
case ReturnKeyType::None:
case ReturnKeyType::Previous:
return UIReturnKeyDefault;
}
}
UITextContentType RCTUITextContentTypeFromString(const std::string &contentType)
{
static dispatch_once_t onceToken;
static NSDictionary<NSString *, NSString *> *contentTypeMap;
dispatch_once(&onceToken, ^{
NSMutableDictionary<NSString *, NSString *> *mutableContentTypeMap = [NSMutableDictionary new];
[mutableContentTypeMap addEntriesFromDictionary:@{
@"" : @"",
@"none" : @"",
@"URL" : UITextContentTypeURL,
@"addressCity" : UITextContentTypeAddressCity,
@"addressCityAndState" : UITextContentTypeAddressCityAndState,
@"addressState" : UITextContentTypeAddressState,
@"countryName" : UITextContentTypeCountryName,
@"creditCardNumber" : UITextContentTypeCreditCardNumber,
@"emailAddress" : UITextContentTypeEmailAddress,
@"familyName" : UITextContentTypeFamilyName,
@"fullStreetAddress" : UITextContentTypeFullStreetAddress,
@"givenName" : UITextContentTypeGivenName,
@"jobTitle" : UITextContentTypeJobTitle,
@"location" : UITextContentTypeLocation,
@"middleName" : UITextContentTypeMiddleName,
@"name" : UITextContentTypeName,
@"namePrefix" : UITextContentTypeNamePrefix,
@"nameSuffix" : UITextContentTypeNameSuffix,
@"nickname" : UITextContentTypeNickname,
@"organizationName" : UITextContentTypeOrganizationName,
@"postalCode" : UITextContentTypePostalCode,
@"streetAddressLine1" : UITextContentTypeStreetAddressLine1,
@"streetAddressLine2" : UITextContentTypeStreetAddressLine2,
@"sublocality" : UITextContentTypeSublocality,
@"telephoneNumber" : UITextContentTypeTelephoneNumber,
@"username" : UITextContentTypeUsername,
@"password" : UITextContentTypePassword,
@"newPassword" : UITextContentTypeNewPassword,
@"oneTimeCode" : UITextContentTypeOneTimeCode,
}];
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000 /* __IPHONE_17_0 */
if (@available(iOS 17.0, *)) {
[mutableContentTypeMap addEntriesFromDictionary:@{
@"creditCardExpiration" : UITextContentTypeCreditCardExpiration,
@"creditCardExpirationMonth" : UITextContentTypeCreditCardExpirationMonth,
@"creditCardExpirationYear" : UITextContentTypeCreditCardExpirationYear,
@"creditCardSecurityCode" : UITextContentTypeCreditCardSecurityCode,
@"creditCardType" : UITextContentTypeCreditCardType,
@"creditCardName" : UITextContentTypeCreditCardName,
@"creditCardGivenName" : UITextContentTypeCreditCardGivenName,
@"creditCardMiddleName" : UITextContentTypeCreditCardMiddleName,
@"creditCardFamilyName" : UITextContentTypeCreditCardFamilyName,
@"birthdate" : UITextContentTypeBirthdate,
@"birthdateDay" : UITextContentTypeBirthdateDay,
@"birthdateMonth" : UITextContentTypeBirthdateMonth,
@"birthdateYear" : UITextContentTypeBirthdateYear,
}];
}
#endif
contentTypeMap = mutableContentTypeMap;
});
return contentTypeMap[RCTNSStringFromString(contentType)] ?: @"";
}
UITextInputPasswordRules *RCTUITextInputPasswordRulesFromString(const std::string &passwordRules)
{
return [UITextInputPasswordRules passwordRulesWithDescriptor:RCTNSStringFromStringNilIfEmpty(passwordRules)];
}
UITextSmartInsertDeleteType RCTUITextSmartInsertDeleteTypeFromOptionalBool(std::optional<bool> smartInsertDelete)
{
return smartInsertDelete.has_value()
? (*smartInsertDelete ? UITextSmartInsertDeleteTypeYes : UITextSmartInsertDeleteTypeNo)
: UITextSmartInsertDeleteTypeDefault;
}

View File

@@ -0,0 +1,18 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTUnimplementedNativeComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,59 @@
/*
* 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 "RCTUnimplementedNativeComponentView.h"
#import <react/renderer/components/rncore/ComponentDescriptors.h>
#import <react/renderer/components/rncore/EventEmitters.h>
#import <react/renderer/components/rncore/Props.h>
using namespace facebook::react;
@implementation RCTUnimplementedNativeComponentView {
UILabel *_label;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = UnimplementedNativeViewShadowNode::defaultSharedProps();
CGRect bounds = self.bounds;
_label = [[UILabel alloc] initWithFrame:bounds];
_label.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.3];
_label.layoutMargins = UIEdgeInsetsMake(12, 12, 12, 12);
_label.lineBreakMode = NSLineBreakByWordWrapping;
_label.numberOfLines = 0;
_label.textAlignment = NSTextAlignmentCenter;
_label.textColor = [UIColor whiteColor];
self.contentView = _label;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<UnimplementedNativeViewComponentDescriptor>();
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldViewProps = static_cast<const UnimplementedNativeViewProps &>(*_props);
const auto &newViewProps = static_cast<const UnimplementedNativeViewProps &>(*props);
if (oldViewProps.name != newViewProps.name) {
_label.text = [NSString stringWithFormat:@"'%s' is not Fabric compatible yet.", newViewProps.name.c_str()];
}
[super updateProps:props oldProps:oldProps];
}
@end

View File

@@ -0,0 +1,21 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
/*
* UIView class for all kinds of <UnimplementedView> components.
*/
@interface RCTUnimplementedViewComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,72 @@
/*
* 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 "RCTUnimplementedViewComponentView.h"
#import <react/renderer/components/rncore/ComponentDescriptors.h>
#import <react/renderer/components/rncore/EventEmitters.h>
#import <react/renderer/components/rncore/Props.h>
#import <react/renderer/components/unimplementedview/UnimplementedViewComponentDescriptor.h>
#import <react/renderer/components/unimplementedview/UnimplementedViewShadowNode.h>
#import <React/RCTConversions.h>
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@implementation RCTUnimplementedViewComponentView {
UILabel *_label;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = UnimplementedViewShadowNode::defaultSharedProps();
_label = [[UILabel alloc] initWithFrame:self.bounds];
_label.backgroundColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:0.3];
_label.lineBreakMode = NSLineBreakByCharWrapping;
_label.numberOfLines = 0;
_label.textAlignment = NSTextAlignmentCenter;
_label.textColor = [UIColor whiteColor];
_label.allowsDefaultTighteningForTruncation = YES;
_label.adjustsFontSizeToFitWidth = YES;
self.contentView = _label;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<UnimplementedViewComponentDescriptor>();
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &oldUnimplementedViewProps = static_cast<const UnimplementedViewProps &>(*_props);
const auto &newUnimplementedViewProps = static_cast<const UnimplementedViewProps &>(*props);
if (oldUnimplementedViewProps.getComponentName() != newUnimplementedViewProps.getComponentName()) {
_label.text =
[NSString stringWithFormat:@"Unimplemented component: <%s>", newUnimplementedViewProps.getComponentName()];
}
[super updateProps:props oldProps:oldProps];
}
@end
Class<RCTComponentViewProtocol> RCTUnimplementedNativeViewCls(void)
{
return RCTUnimplementedViewComponentView.class;
}

View File

@@ -0,0 +1,85 @@
/*
* 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 <UIKit/UIKit.h>
#import <React/RCTComponentViewProtocol.h>
#import <React/RCTConstants.h>
#import <React/RCTTouchableComponentViewProtocol.h>
#import <React/UIView+ComponentViewProtocol.h>
#import <react/renderer/components/view/ViewEventEmitter.h>
#import <react/renderer/components/view/ViewProps.h>
#import <react/renderer/core/EventEmitter.h>
#import <react/renderer/core/LayoutMetrics.h>
#import <react/renderer/core/Props.h>
NS_ASSUME_NONNULL_BEGIN
/**
* UIView class for <View> component.
*/
@interface RCTViewComponentView : UIView <RCTComponentViewProtocol, RCTTouchableComponentViewProtocol> {
@protected
facebook::react::LayoutMetrics _layoutMetrics;
facebook::react::SharedViewProps _props;
facebook::react::SharedViewEventEmitter _eventEmitter;
}
/**
* Represents the `UIView` instance that is being automatically attached to
* the component view and laid out using on `layoutMetrics` (especially `size`
* and `padding`) of the component.
* This view must not be a component view; it's just a convenient way
* to embed/bridge pure native views as component views.
* Defaults to `nil`. Assign `nil` to remove view as subview.
*/
@property (nonatomic, strong, nullable) UIView *contentView;
/**
* Provides access to `nativeId` prop of the component.
* It might be used by subclasses (which need to refer to the view from
* other platform-specific external views or systems by some id) or
* by debugging/inspection tools.
* Defaults to `nil`.
*/
@property (nonatomic, strong, nullable) NSString *nativeId;
/**
* Returns the object - usually (sub)view - which represents this
* component view in terms of accessibility.
* All accessibility properties will be applied to this object.
* May be overridden in subclass which needs to be accessiblitywise
* transparent in favour of some subview.
* Defaults to `self`.
*/
@property (nonatomic, strong, nullable, readonly) NSObject *accessibilityElement;
/**
* Insets used when hit testing inside this view.
*/
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;
/**
* Enforcing `call super` semantic for overridden methods from `RCTComponentViewProtocol`.
* The methods update the instance variables.
*/
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps NS_REQUIRES_SUPER;
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter NS_REQUIRES_SUPER;
- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics NS_REQUIRES_SUPER;
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask NS_REQUIRES_SUPER;
- (void)prepareForRecycle NS_REQUIRES_SUPER;
/*
* This is a fragment of temporary workaround that we need only temporary and will get rid of soon.
*/
- (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,929 @@
/*
* 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 "RCTViewComponentView.h"
#import <CoreGraphics/CoreGraphics.h>
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
#import <React/RCTAssert.h>
#import <React/RCTBorderDrawing.h>
#import <React/RCTConversions.h>
#import <React/RCTLocalizedString.h>
#import <react/renderer/components/view/ViewComponentDescriptor.h>
#import <react/renderer/components/view/ViewEventEmitter.h>
#import <react/renderer/components/view/ViewProps.h>
#import <react/renderer/components/view/accessibilityPropsConversions.h>
#ifdef RCT_DYNAMIC_FRAMEWORKS
#import <React/RCTComponentViewFactory.h>
#endif
using namespace facebook::react;
@implementation RCTViewComponentView {
UIColor *_backgroundColor;
__weak CALayer *_borderLayer;
BOOL _needsInvalidateLayer;
BOOL _isJSResponder;
BOOL _removeClippedSubviews;
NSMutableArray<UIView *> *_reactSubviews;
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
}
#ifdef RCT_DYNAMIC_FRAMEWORKS
+ (void)load
{
[RCTComponentViewFactory.currentComponentViewFactory registerComponentViewClass:self];
}
#endif
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ViewShadowNode::defaultSharedProps();
_reactSubviews = [NSMutableArray new];
self.multipleTouchEnabled = YES;
}
return self;
}
- (facebook::react::Props::Shared)props
{
return _props;
}
- (void)setContentView:(UIView *)contentView
{
if (_contentView) {
[_contentView removeFromSuperview];
}
_contentView = contentView;
if (_contentView) {
[self addSubview:_contentView];
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
}
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
return [super pointInside:point withEvent:event];
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
- (UIColor *)backgroundColor
{
return _backgroundColor;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
_backgroundColor = backgroundColor;
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
[self invalidateLayer];
}
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
RCTAssert(
self == [RCTViewComponentView class],
@"`+[RCTComponentViewProtocol componentDescriptorProvider]` must be implemented for all subclasses (and `%@` particularly).",
NSStringFromClass([self class]));
return concreteComponentDescriptorProvider<ViewComponentDescriptor>();
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
childComponentView.superview == nil,
@"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)",
self,
childComponentView,
@(index),
@([childComponentView.superview tag]));
if (_removeClippedSubviews) {
[_reactSubviews insertObject:childComponentView atIndex:index];
} else {
[self insertSubview:childComponentView atIndex:index];
}
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if (_removeClippedSubviews) {
[_reactSubviews removeObjectAtIndex:index];
} else {
RCTAssert(
childComponentView.superview == self,
@"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)",
self,
childComponentView,
@(index));
RCTAssert(
(self.subviews.count > index) && [self.subviews objectAtIndex:index] == childComponentView,
@"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@, actual index: %@, tag at index: %@)",
self,
childComponentView,
@(index),
@([self.subviews indexOfObject:childComponentView]),
@([[self.subviews objectAtIndex:index] tag]));
}
[childComponentView removeFromSuperview];
}
- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
{
if (!_removeClippedSubviews) {
// Use default behavior if unmounting is disabled
return [super updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
}
if (_reactSubviews.count == 0) {
// Do nothing if we have no subviews
return;
}
if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
// Do nothing if layout hasn't happened yet
return;
}
// Convert clipping rect to local coordinates
clipRect = [clipView convertRect:clipRect toView:self];
// Mount / unmount views
for (UIView *view in _reactSubviews) {
if (CGRectIntersectsRect(clipRect, view.frame)) {
// View is at least partially visible, so remount it if unmounted
[self addSubview:view];
// View is visible, update clipped subviews
[view updateClippedSubviewsWithClipRect:clipRect relativeToView:self];
} else if (view.superview) {
// View is completely outside the clipRect, so unmount it
[view removeFromSuperview];
}
}
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
RCTAssert(props, @"`props` must not be `null`.");
#ifndef NS_BLOCK_ASSERTIONS
auto propsRawPtr = _props.get();
RCTAssert(
propsRawPtr &&
([self class] == [RCTViewComponentView class] ||
typeid(*propsRawPtr).hash_code() != typeid(ViewProps const).hash_code()),
@"`RCTViewComponentView` subclasses (and `%@` particularly) must setup `_props`"
" instance variable with a default value in the constructor.",
NSStringFromClass([self class]));
#endif
const auto &oldViewProps = static_cast<const ViewProps &>(*_props);
const auto &newViewProps = static_cast<const ViewProps &>(*props);
BOOL needsInvalidateLayer = NO;
// `opacity`
if (oldViewProps.opacity != newViewProps.opacity &&
![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"opacity"]) {
self.layer.opacity = (float)newViewProps.opacity;
needsInvalidateLayer = YES;
}
if (oldViewProps.removeClippedSubviews != newViewProps.removeClippedSubviews) {
_removeClippedSubviews = newViewProps.removeClippedSubviews;
if (_removeClippedSubviews && self.subviews.count > 0) {
_reactSubviews = [NSMutableArray arrayWithArray:self.subviews];
}
}
// `backgroundColor`
if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
self.backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor);
needsInvalidateLayer = YES;
}
// `shadowColor`
if (oldViewProps.shadowColor != newViewProps.shadowColor) {
CGColorRef shadowColor = RCTCreateCGColorRefFromSharedColor(newViewProps.shadowColor);
self.layer.shadowColor = shadowColor;
CGColorRelease(shadowColor);
needsInvalidateLayer = YES;
}
// `shadowOffset`
if (oldViewProps.shadowOffset != newViewProps.shadowOffset) {
self.layer.shadowOffset = RCTCGSizeFromSize(newViewProps.shadowOffset);
needsInvalidateLayer = YES;
}
// `shadowOpacity`
if (oldViewProps.shadowOpacity != newViewProps.shadowOpacity) {
self.layer.shadowOpacity = (float)newViewProps.shadowOpacity;
needsInvalidateLayer = YES;
}
// `shadowRadius`
if (oldViewProps.shadowRadius != newViewProps.shadowRadius) {
self.layer.shadowRadius = (CGFloat)newViewProps.shadowRadius;
needsInvalidateLayer = YES;
}
// `backfaceVisibility`
if (oldViewProps.backfaceVisibility != newViewProps.backfaceVisibility) {
self.layer.doubleSided = newViewProps.backfaceVisibility == BackfaceVisibility::Visible;
}
// `cursor`
if (oldViewProps.cursor != newViewProps.cursor) {
needsInvalidateLayer = YES;
}
// `shouldRasterize`
if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) {
self.layer.shouldRasterize = newViewProps.shouldRasterize;
self.layer.rasterizationScale = newViewProps.shouldRasterize ? self.traitCollection.displayScale : 1.0;
}
// `pointerEvents`
if (oldViewProps.pointerEvents != newViewProps.pointerEvents) {
self.userInteractionEnabled = newViewProps.pointerEvents != PointerEventsMode::None;
}
// `transform`
if ((oldViewProps.transform != newViewProps.transform ||
oldViewProps.transformOrigin != newViewProps.transformOrigin) &&
![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
auto newTransform = newViewProps.resolveTransform(_layoutMetrics);
CATransform3D caTransform = RCTCATransform3DFromTransformMatrix(newTransform);
self.layer.transform = caTransform;
// Enable edge antialiasing in rotation, skew, or perspective transforms
self.layer.allowsEdgeAntialiasing = caTransform.m12 != 0.0f || caTransform.m21 != 0.0f || caTransform.m34 != 0.0f;
}
// `hitSlop`
if (oldViewProps.hitSlop != newViewProps.hitSlop) {
self.hitTestEdgeInsets = {
-newViewProps.hitSlop.top,
-newViewProps.hitSlop.left,
-newViewProps.hitSlop.bottom,
-newViewProps.hitSlop.right};
}
// `overflow`
if (oldViewProps.getClipsContentToBounds() != newViewProps.getClipsContentToBounds()) {
self.clipsToBounds = newViewProps.getClipsContentToBounds();
needsInvalidateLayer = YES;
}
// `border`
if (oldViewProps.borderStyles != newViewProps.borderStyles || oldViewProps.borderRadii != newViewProps.borderRadii ||
oldViewProps.borderColors != newViewProps.borderColors) {
needsInvalidateLayer = YES;
}
// `nativeId`
if (oldViewProps.nativeId != newViewProps.nativeId) {
self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId);
}
// `accessible`
if (oldViewProps.accessible != newViewProps.accessible) {
self.accessibilityElement.isAccessibilityElement = newViewProps.accessible;
}
// `accessibilityLabel`
if (oldViewProps.accessibilityLabel != newViewProps.accessibilityLabel) {
self.accessibilityElement.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLabel);
}
// `accessibilityLanguage`
if (oldViewProps.accessibilityLanguage != newViewProps.accessibilityLanguage) {
self.accessibilityElement.accessibilityLanguage =
RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLanguage);
}
// `accessibilityHint`
if (oldViewProps.accessibilityHint != newViewProps.accessibilityHint) {
self.accessibilityElement.accessibilityHint = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint);
}
// `accessibilityViewIsModal`
if (oldViewProps.accessibilityViewIsModal != newViewProps.accessibilityViewIsModal) {
self.accessibilityElement.accessibilityViewIsModal = newViewProps.accessibilityViewIsModal;
}
// `accessibilityElementsHidden`
if (oldViewProps.accessibilityElementsHidden != newViewProps.accessibilityElementsHidden) {
self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden;
}
// `accessibilityTraits`
if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) {
self.accessibilityElement.accessibilityTraits =
RCTUIAccessibilityTraitsFromAccessibilityTraits(newViewProps.accessibilityTraits);
}
// `accessibilityState`
if (oldViewProps.accessibilityState != newViewProps.accessibilityState) {
self.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected);
const auto accessibilityState = newViewProps.accessibilityState.value_or(AccessibilityState{});
if (accessibilityState.selected) {
self.accessibilityTraits |= UIAccessibilityTraitSelected;
}
if (accessibilityState.disabled) {
self.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
}
}
// `accessibilityIgnoresInvertColors`
if (oldViewProps.accessibilityIgnoresInvertColors != newViewProps.accessibilityIgnoresInvertColors) {
self.accessibilityIgnoresInvertColors = newViewProps.accessibilityIgnoresInvertColors;
}
// `accessibilityValue`
if (oldViewProps.accessibilityValue != newViewProps.accessibilityValue) {
if (newViewProps.accessibilityValue.text.has_value()) {
self.accessibilityElement.accessibilityValue =
RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityValue.text.value());
} else if (
newViewProps.accessibilityValue.now.has_value() && newViewProps.accessibilityValue.min.has_value() &&
newViewProps.accessibilityValue.max.has_value()) {
CGFloat val = (CGFloat)(newViewProps.accessibilityValue.now.value()) /
(newViewProps.accessibilityValue.max.value() - newViewProps.accessibilityValue.min.value());
self.accessibilityElement.accessibilityValue =
[NSNumberFormatter localizedStringFromNumber:@(val) numberStyle:NSNumberFormatterPercentStyle];
;
} else {
self.accessibilityElement.accessibilityValue = nil;
}
}
// `testId`
if (oldViewProps.testId != newViewProps.testId) {
self.accessibilityIdentifier = RCTNSStringFromString(newViewProps.testId);
}
_needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer;
_props = std::static_pointer_cast<const ViewProps>(props);
}
- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
{
assert(std::dynamic_pointer_cast<const ViewEventEmitter>(eventEmitter));
_eventEmitter = std::static_pointer_cast<const ViewEventEmitter>(eventEmitter);
}
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
// Using stored `_layoutMetrics` as `oldLayoutMetrics` here to avoid
// re-applying individual sub-values which weren't changed.
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics];
_layoutMetrics = layoutMetrics;
_needsInvalidateLayer = YES;
_borderLayer.frame = self.layer.bounds;
if (_contentView) {
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
}
if (_props->transformOrigin.isSet()) {
auto newTransform = _props->resolveTransform(layoutMetrics);
self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
}
}
- (BOOL)isJSResponder
{
return _isJSResponder;
}
- (void)setIsJSResponder:(BOOL)isJSResponder
{
_isJSResponder = isJSResponder;
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
if (!_needsInvalidateLayer) {
return;
}
_needsInvalidateLayer = NO;
[self invalidateLayer];
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
// If view was managed by animated, its props need to align with UIView's properties.
const auto &props = static_cast<const ViewProps &>(*_props);
if ([_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
self.layer.transform = RCTCATransform3DFromTransformMatrix(props.transform);
}
if ([_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"opacity"]) {
self.layer.opacity = (float)props.opacity;
}
_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil;
_eventEmitter.reset();
_isJSResponder = NO;
_removeClippedSubviews = NO;
_reactSubviews = [NSMutableArray new];
}
- (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet<NSString *> *_Nullable)props
{
_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = props;
}
- (NSSet<NSString *> *_Nullable)propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN
{
return _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
}
- (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// This is a classic textbook implementation of `hitTest:` with a couple of improvements:
// * It does not stop algorithm if some touch is outside the view
// which does not have `clipToBounds` enabled.
// * Taking `layer.zIndex` field into an account is not required because
// lists of `ShadowView`s are already sorted based on `zIndex` prop.
if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
return nil;
}
BOOL isPointInside = [self pointInside:point withEvent:event];
BOOL clipsToBounds = self.clipsToBounds;
clipsToBounds = clipsToBounds || _layoutMetrics.overflowInset == EdgeInsets{};
if (clipsToBounds && !isPointInside) {
return nil;
}
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
if (hitView) {
return hitView;
}
}
return isPointInside ? self : nil;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
switch (_props->pointerEvents) {
case PointerEventsMode::Auto:
return [self betterHitTest:point withEvent:event];
case PointerEventsMode::None:
return nil;
case PointerEventsMode::BoxOnly:
return [self pointInside:point withEvent:event] ? self : nil;
case PointerEventsMode::BoxNone:
UIView *view = [self betterHitTest:point withEvent:event];
return view != self ? view : nil;
}
}
static RCTCornerRadii RCTCornerRadiiFromBorderRadii(BorderRadii borderRadii)
{
return RCTCornerRadii{
.topLeft = (CGFloat)borderRadii.topLeft,
.topRight = (CGFloat)borderRadii.topRight,
.bottomLeft = (CGFloat)borderRadii.bottomLeft,
.bottomRight = (CGFloat)borderRadii.bottomRight};
}
static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors borderColors)
{
return RCTBorderColors{
.top = RCTCreateCGColorRefFromSharedColor(borderColors.top),
.left = RCTCreateCGColorRefFromSharedColor(borderColors.left),
.bottom = RCTCreateCGColorRefFromSharedColor(borderColors.bottom),
.right = RCTCreateCGColorRefFromSharedColor(borderColors.right)};
}
static void RCTReleaseRCTBorderColors(RCTBorderColors borderColors)
{
CGColorRelease(borderColors.top);
CGColorRelease(borderColors.left);
CGColorRelease(borderColors.bottom);
CGColorRelease(borderColors.right);
}
static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve)
{
// The constants are available only starting from iOS 13
// CALayerCornerCurve is a typealias on NSString *
switch (borderCurve) {
case BorderCurve::Continuous:
return @"continuous"; // kCACornerCurveContinuous;
case BorderCurve::Circular:
return @"circular"; // kCACornerCurveCircular;
}
}
static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
{
switch (borderStyle) {
case BorderStyle::Solid:
return RCTBorderStyleSolid;
case BorderStyle::Dotted:
return RCTBorderStyleDotted;
case BorderStyle::Dashed:
return RCTBorderStyleDashed;
}
}
- (void)invalidateLayer
{
CALayer *layer = self.layer;
if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) {
return;
}
const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
// Stage 1. Shadow Path
BOOL const layerHasShadow = layer.shadowOpacity > 0 && CGColorGetAlpha(layer.shadowColor) > 0;
if (layerHasShadow) {
if (CGColorGetAlpha(_backgroundColor.CGColor) > 0.999) {
// If view has a solid background color, calculate shadow path from border.
const RCTCornerInsets cornerInsets =
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil);
layer.shadowPath = shadowPath;
CGPathRelease(shadowPath);
} else {
// Can't accurately calculate box shadow, so fall back to pixel-based shadow.
layer.shadowPath = nil;
}
} else {
layer.shadowPath = nil;
}
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000 /* __IPHONE_17_0 */
// Stage 1.5. Cursor / Hover Effects
if (@available(iOS 17.0, *)) {
UIHoverStyle *hoverStyle = nil;
if (_props->cursor == Cursor::Pointer) {
const RCTCornerInsets cornerInsets =
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
#if TARGET_OS_IOS
// Due to an Apple bug, it seems on iOS, UIShapes made with `[UIShape shapeWithBezierPath:]`
// evaluate their shape on the superviews' coordinate space. This leads to the hover shape
// rendering incorrectly on iOS, iOS apps in compatibility mode on visionOS, but not on visionOS.
// To work around this, for iOS, we can calculate the border path based on `view.frame` (the
// superview's coordinate space) instead of view.bounds.
CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.frame, cornerInsets, NULL);
#else // TARGET_OS_VISION
CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL);
#endif
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath];
CGPathRelease(borderPath);
UIShape *shape = [UIShape shapeWithBezierPath:bezierPath];
hoverStyle = [UIHoverStyle styleWithEffect:[UIHoverAutomaticEffect effect] shape:shape];
}
[self setHoverStyle:hoverStyle];
}
#endif
// Stage 2. Border Rendering
const bool useCoreAnimationBorderRendering =
borderMetrics.borderColors.isUniform() && borderMetrics.borderWidths.isUniform() &&
borderMetrics.borderStyles.isUniform() && borderMetrics.borderRadii.isUniform() &&
borderMetrics.borderStyles.left == BorderStyle::Solid &&
(
// iOS draws borders in front of the content whereas CSS draws them behind
// the content. For this reason, only use iOS border drawing when clipping
// or when the border is hidden.
borderMetrics.borderWidths.left == 0 ||
colorComponentsFromColor(borderMetrics.borderColors.left).alpha == 0 || self.clipsToBounds);
CGColorRef backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor;
if (useCoreAnimationBorderRendering) {
layer.mask = nil;
[_borderLayer removeFromSuperlayer];
layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left;
CGColorRef borderColor = RCTCreateCGColorRefFromSharedColor(borderMetrics.borderColors.left);
layer.borderColor = borderColor;
CGColorRelease(borderColor);
layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft;
layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
layer.backgroundColor = backgroundColor;
} else {
if (!_borderLayer) {
CALayer *borderLayer = [CALayer new];
borderLayer.zPosition = -1024.0f;
borderLayer.frame = layer.bounds;
borderLayer.magnificationFilter = kCAFilterNearest;
[layer addSublayer:borderLayer];
_borderLayer = borderLayer;
}
layer.backgroundColor = nil;
layer.borderWidth = 0;
layer.borderColor = nil;
layer.cornerRadius = 0;
RCTBorderColors borderColors = RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors);
UIImage *image = RCTGetBorderImage(
RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left),
layer.bounds.size,
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
borderColors,
backgroundColor,
self.clipsToBounds);
RCTReleaseRCTBorderColors(borderColors);
if (image == nil) {
_borderLayer.contents = nil;
} else {
CGSize imageSize = image.size;
UIEdgeInsets imageCapInsets = image.capInsets;
CGRect contentsCenter = CGRect{
CGPoint{imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height},
CGSize{(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height}};
_borderLayer.contents = (id)image.CGImage;
_borderLayer.contentsScale = image.scale;
BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
if (isResizable) {
_borderLayer.contentsCenter = contentsCenter;
} else {
_borderLayer.contentsCenter = CGRect{CGPoint{0.0, 0.0}, CGSize{1.0, 1.0}};
}
}
// If mutations are applied inside of Animation block, it may cause _borderLayer to be animated.
// To stop that, imperatively remove all animations from _borderLayer.
[_borderLayer removeAllAnimations];
// Stage 2.5. Custom Clipping Mask
CAShapeLayer *maskLayer = nil;
CGFloat cornerRadius = 0;
if (self.clipsToBounds) {
if (borderMetrics.borderRadii.isUniform()) {
// In this case we can simply use `cornerRadius` exclusively.
cornerRadius = borderMetrics.borderRadii.topLeft;
} else {
// In this case we have to generate masking layer manually.
CGPathRef path = RCTPathCreateWithRoundedRect(
self.bounds,
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero),
nil);
maskLayer = [CAShapeLayer layer];
maskLayer.path = path;
CGPathRelease(path);
}
}
layer.cornerRadius = cornerRadius;
layer.mask = maskLayer;
}
}
#pragma mark - Accessibility
- (NSObject *)accessibilityElement
{
return self;
}
static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
{
NSMutableString *result = [NSMutableString stringWithString:@""];
for (UIView *subview in view.subviews) {
NSString *label = subview.accessibilityLabel;
if (!label) {
label = RCTRecursiveAccessibilityLabel(subview);
}
if (label && label.length > 0) {
if (result.length > 0) {
[result appendString:@" "];
}
[result appendString:label];
}
}
return result;
}
- (NSString *)accessibilityLabel
{
NSString *label = super.accessibilityLabel;
if (label) {
return label;
}
return RCTRecursiveAccessibilityLabel(self);
}
- (NSString *)accessibilityValue
{
const auto &props = static_cast<const ViewProps &>(*_props);
const auto accessibilityState = props.accessibilityState.value_or(AccessibilityState{});
// Handle Switch.
if ((self.accessibilityTraits & AccessibilityTraitSwitch) == AccessibilityTraitSwitch) {
if (accessibilityState.checked == AccessibilityState::Checked) {
return @"1";
} else if (accessibilityState.checked == AccessibilityState::Unchecked) {
return @"0";
}
}
NSMutableArray *valueComponents = [NSMutableArray new];
NSString *roleString = (props.role != Role::None) ? [NSString stringWithUTF8String:toString(props.role).c_str()]
: [NSString stringWithUTF8String:props.accessibilityRole.c_str()];
// In iOS, checkbox and radio buttons aren't recognized as traits. However,
// because our apps use checkbox and radio buttons often, we should announce
// these to screenreader users. (They should already be familiar with them
// from using web).
if ([roleString isEqualToString:@"checkbox"]) {
[valueComponents addObject:RCTLocalizedString("checkbox", "checkable interactive control")];
}
if ([roleString isEqualToString:@"radio"]) {
[valueComponents
addObject:
RCTLocalizedString(
"radio button",
"a checkable input that when associated with other radio buttons, only one of which can be checked at a time")];
}
// Handle states which haven't already been handled.
if (accessibilityState.checked == AccessibilityState::Checked) {
[valueComponents
addObject:RCTLocalizedString("checked", "a checkbox, radio button, or other widget which is checked")];
}
if (accessibilityState.checked == AccessibilityState::Unchecked) {
[valueComponents
addObject:RCTLocalizedString("unchecked", "a checkbox, radio button, or other widget which is unchecked")];
}
if (accessibilityState.checked == AccessibilityState::Mixed) {
[valueComponents
addObject:RCTLocalizedString(
"mixed", "a checkbox, radio button, or other widget which is both checked and unchecked")];
}
if (accessibilityState.expanded.value_or(false)) {
[valueComponents
addObject:RCTLocalizedString("expanded", "a menu, dialog, accordian panel, or other widget which is expanded")];
}
if (accessibilityState.busy) {
[valueComponents addObject:RCTLocalizedString("busy", "an element currently being updated or modified")];
}
// Using super.accessibilityValue:
// 1. to access the value that is set to accessibilityValue in updateProps
// 2. can't access from self.accessibilityElement because it resolves to self
if (super.accessibilityValue) {
[valueComponents addObject:super.accessibilityValue];
}
if (valueComponents.count > 0) {
return [valueComponents componentsJoinedByString:@", "];
}
return nil;
}
#pragma mark - Accessibility Events
- (BOOL)shouldGroupAccessibilityChildren
{
return YES;
}
- (NSArray<UIAccessibilityCustomAction *> *)accessibilityCustomActions
{
const auto &accessibilityActions = _props->accessibilityActions;
if (accessibilityActions.empty()) {
return nil;
}
NSMutableArray<UIAccessibilityCustomAction *> *customActions = [NSMutableArray array];
for (const auto &accessibilityAction : accessibilityActions) {
[customActions
addObject:[[UIAccessibilityCustomAction alloc] initWithName:RCTNSStringFromString(accessibilityAction.name)
target:self
selector:@selector(didActivateAccessibilityCustomAction:)]];
}
return [customActions copy];
}
- (BOOL)accessibilityActivate
{
if (_eventEmitter && _props->onAccessibilityTap) {
_eventEmitter->onAccessibilityTap();
return YES;
} else {
return NO;
}
}
- (BOOL)accessibilityPerformMagicTap
{
if (_eventEmitter && _props->onAccessibilityMagicTap) {
_eventEmitter->onAccessibilityMagicTap();
return YES;
} else {
return NO;
}
}
- (BOOL)accessibilityPerformEscape
{
if (_eventEmitter && _props->onAccessibilityEscape) {
_eventEmitter->onAccessibilityEscape();
return YES;
} else {
return NO;
}
}
- (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action
{
if (_eventEmitter && _props->onAccessibilityAction) {
_eventEmitter->onAccessibilityAction(RCTStringFromNSString(action.name));
return YES;
} else {
return NO;
}
}
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
{
return _eventEmitter;
}
- (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN
{
return RCTNSStringFromString([[self class] componentDescriptorProvider].name);
}
@end
#ifdef __cplusplus
extern "C" {
#endif
// Can't the import generated Plugin.h because plugins are not in this BUCK target
Class<RCTComponentViewProtocol> RCTViewCls(void);
#ifdef __cplusplus
}
#endif
Class<RCTComponentViewProtocol> RCTViewCls(void)
{
return RCTViewComponentView.class;
}