first commit

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

View File

@@ -0,0 +1,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/FBReactNativeSpec/ComponentDescriptors.h>
#import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/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/FBReactNativeSpec/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/FBReactNativeSpec/ComponentDescriptors.h>
#import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/Props.h>
#import <react/renderer/components/FBReactNativeSpec/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,228 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "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>
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];
}
- (NSObject *)accessibilityElement
{
return _imageView;
}
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
{
RCTAssert(state, @"`state` must not be null.");
RCTAssert(
std::dynamic_pointer_cast<const ImageShadowNode::ConcreteState>(state),
@"`state` must be a pointer to `ImageShadowNode::ConcreteState`.");
auto oldImageState = std::static_pointer_cast<const ImageShadowNode::ConcreteState>(_state);
auto newImageState = std::static_pointer_cast<const ImageShadowNode::ConcreteState>(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);
}
_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;
}
auto imageSource = _state->getData().getImageSource();
imageSource.size = {.width = image.size.width, .height = image.size.height};
static_cast<const ImageEventEmitter &>(*_eventEmitter).onLoad(imageSource);
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
loaded:(int64_t)loaded
total:(int64_t)total
fromObserver:(const void *)observer
{
if (!_eventEmitter) {
return;
}
static_cast<const ImageEventEmitter &>(*_eventEmitter).onProgress(progress, loaded, total);
}
- (void)didReceiveFailure:(NSError *)error fromObserver:(const void *)observer
{
_imageView.image = nil;
if (!_eventEmitter) {
return;
}
ImageErrorInfo info;
if (error) {
info.error = std::string([error.localizedDescription UTF8String]);
id code = error.userInfo[@"httpStatusCode"];
if (code) {
info.responseCode = [code intValue];
}
id rspHeaders = error.userInfo[@"httpResponseHeaders"];
if (rspHeaders) {
for (NSString *key in rspHeaders) {
id value = rspHeaders[key];
info.httpResponseHeaders.push_back(
std::pair<std::string, std::string>(std::string([key UTF8String]), std::string([value UTF8String])));
}
}
}
static_cast<const ImageEventEmitter &>(*_eventEmitter).onError(ImageErrorInfo(info));
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/FBReactNativeSpec/Props.h>
#import <react/renderer/components/inputaccessory/InputAccessoryComponentDescriptor.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<const InputAccessoryShadowNode::ConcreteState>(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,56 @@
/*
* 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.leadingAnchor],
[_safeAreaContainer.trailingAnchor constraintEqualToAnchor:self.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];
}
@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,290 @@
/*
* 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",
@"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> *supported = [NSMutableDictionary new];
return supported;
}
+ (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
{
if (_adapter && index == _adapter.paperView.reactSubviews.count) {
// This is a new child view that is being added to the end of the children array.
// After the children is added, we need to call didUpdateReactSubviews to make sure that it is rendered.
// Without this change, the new child will not be rendered right away because the didUpdateReactSubviews is not
// called and the `finalizeUpdate` is not invoked.
if ([childComponentView isKindOfClass:[RCTLegacyViewManagerInteropComponentView class]]) {
UIView *target = ((RCTLegacyViewManagerInteropComponentView *)childComponentView).contentView;
[_adapter.paperView insertReactSubview:target atIndex:index];
} else {
[_adapter.paperView insertReactSubview:childComponentView atIndex:index];
}
[_adapter.paperView didUpdateReactSubviews];
} else {
[_viewsToBeMounted addObject:@{
kRCTLegacyInteropChildIndexKey : [NSNumber numberWithInteger:index],
kRCTLegacyInteropChildComponentKey : childComponentView
}];
}
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if (_adapter && index < _adapter.paperView.reactSubviews.count) {
[_adapter.paperView removeReactSubview:_adapter.paperView.reactSubviews[index]];
} 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<const LegacyViewManagerInteropShadowNode::ConcreteState>(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<const ViewEventEmitter &>(*strongSelf->_eventEmitter);
eventEmitter.dispatchEvent(eventName, std::move(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/UIView+React.h>
#import <react/utils/FollyConvert.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, std::move(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,78 @@
/*
* 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]) == nullptr) {
return nil;
}
_touchHandler = [RCTSurfaceTouchHandler new];
return self;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!CGRectEqualToRect(_lastViewBounds, self.view.bounds)) {
[_delegate boundsDidChange:self.view.bounds];
_lastViewBounds = self.view.bounds;
}
}
- (void)loadView
{
self.view = [UIView new];
[_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 && TARGET_OS_IOS
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
UIInterfaceOrientationMask appSupportedOrientationsMask =
[RCTSharedApplication() supportedInterfaceOrientationsForWindow:RCTKeyWindow()];
if ((_supportedInterfaceOrientations & appSupportedOrientationsMask) == 0u) {
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,31 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import <React/RCTViewComponentView.h>
/**
* UIView class for root <ModalHostView> component.
*/
@interface RCTModalHostViewComponentView : RCTViewComponentView <UIAdaptivePresentationControllerDelegate>
/**
* 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,344 @@
/*
* 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/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/Props.h>
#import <react/renderer/components/modal/ModalHostViewComponentDescriptor.h>
#import <react/renderer/components/modal/ModalHostViewState.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>
@property (nonatomic, weak) UIView *accessibilityFocusedView;
@end
@implementation RCTModalHostViewComponentView {
RCTFabricModalHostViewController *_viewController;
ModalHostViewShadowNode::ConcreteState::Shared _state;
BOOL _shouldAnimatePresentation;
BOOL _shouldPresent;
BOOL _isPresented;
BOOL _modalInPresentation;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ModalHostViewShadowNode::defaultSharedProps();
_shouldAnimatePresentation = YES;
_isPresented = NO;
_modalInPresentation = YES;
}
return self;
}
- (RCTFabricModalHostViewController *)viewController
{
if (!_viewController) {
_viewController = [RCTFabricModalHostViewController new];
_viewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
_viewController.delegate = self;
_viewController.modalInPresentation = _modalInPresentation;
}
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) {
[self saveAccessibilityFocusedView];
self.viewController.presentationController.delegate = self;
self.viewController.modalInPresentation = _modalInPresentation;
_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 = [self.viewController.view snapshotViewAfterScreenUpdates:NO];
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{});
}
[self restoreAccessibilityFocusedView];
}];
}
}
- (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 - UIView methods
- (void)didMoveToWindow
{
[super didMoveToWindow];
[self ensurePresentedOnlyIfNeeded];
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self ensurePresentedOnlyIfNeeded];
}
- (void)saveAccessibilityFocusedView
{
id focusedElement = UIAccessibilityFocusedElement(nil);
if (focusedElement && [focusedElement isKindOfClass:[UIView class]]) {
self.accessibilityFocusedView = (UIView *)focusedElement;
}
}
- (void)restoreAccessibilityFocusedView
{
id viewToFocus = self.accessibilityFocusedView;
if (viewToFocus) {
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, viewToFocus);
self.accessibilityFocusedView = nil;
}
}
#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 &oldViewProps = static_cast<const ModalHostViewProps &>(*_props);
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);
if (oldViewProps.allowSwipeDismissal != newProps.allowSwipeDismissal) {
_modalInPresentation = !newProps.allowSwipeDismissal;
self.viewController.modalInPresentation = _modalInPresentation;
}
_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];
}
#pragma mark - UIAdaptivePresentationControllerDelegate
- (void)presentationControllerDidAttemptToDismiss:(UIPresentationController *)controller
{
auto eventEmitter = [self modalEventEmitter];
if (eventEmitter) {
eventEmitter->onRequestClose({});
}
}
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController
{
auto eventEmitter = [self modalEventEmitter];
const auto &props = static_cast<const ModalHostViewProps &>(*_props);
if (eventEmitter && props.allowSwipeDismissal) {
eventEmitter->onRequestClose({});
}
}
@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,52 @@
/**
* 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/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 nullptr;
}
#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,55 @@
/*
* 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/RCTRootView.h>
#import <react/renderer/components/root/RootComponentDescriptor.h>
#import <react/renderer/components/root/RootProps.h>
#import "RCTConversions.h"
using namespace facebook::react;
@implementation RCTRootComponentView {
BOOL _contentHasAppeared;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = RootShadowNode::defaultSharedProps();
_contentHasAppeared = NO;
}
return self;
}
#pragma mark - RCTComponentViewProtocol
- (void)prepareForRecycle
{
[super prepareForRecycle];
_contentHasAppeared = NO;
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
[super mountChildComponentView:childComponentView index:index];
if (!self->_contentHasAppeared) {
self->_contentHasAppeared = YES;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:RCTContentDidAppearNotification object:self];
});
}
}
+ (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<const SafeAreaViewShadowNode::ConcreteState::Data>(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<const SafeAreaViewShadowNode::ConcreteState>(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,299 @@
/*
* 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>
#import <react/utils/FloatComparison.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.
* This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
*/
- (void)centerContentIfNeeded
{
if (!_centerContent) {
return;
}
CGSize contentSize = self.contentSize;
CGSize boundsSize = self.bounds.size;
if (CGSizeEqualToSize(contentSize, CGSizeZero) || CGSizeEqualToSize(boundsSize, CGSizeZero)) {
return;
}
CGFloat top = 0;
CGFloat left = 0;
if (contentSize.width < boundsSize.width) {
left = (boundsSize.width - contentSize.width) * 0.5f;
}
if (contentSize.height < boundsSize.height) {
top = (boundsSize.height - contentSize.height) * 0.5f;
}
self.contentInset = UIEdgeInsetsMake(top, left, top, left);
}
- (void)setContentOffset:(CGPoint)contentOffset
{
if (_isSetContentOffsetDisabled) {
return;
}
super.contentOffset = CGPointMake(
RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
}
- (void)setFrame:(CGRect)frame
{
[super setFrame:frame];
[self centerContentIfNeeded];
}
- (void)didAddSubview:(UIView *)subview
{
[super didAddSubview:subview];
[self centerContentIfNeeded];
}
- (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;
}
}
}
- (void)scrollViewDidZoom:(__unused UIScrollView *)scrollView
{
[self centerContentIfNeeded];
}
#pragma mark -
- (BOOL)isHorizontal:(UIScrollView *)scrollView
{
return !facebook::react::floatEquality(scrollView.contentSize.width, self.frame.size.width) &&
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,265 @@
/*
* 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/FBReactNativeSpec/ComponentDescriptors.h>
#import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/Props.h>
#import <react/renderer/components/FBReactNativeSpec/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;
// This variable keeps track of whether the view is recycled or not. Once the view is recycled, the component
// creates a new instance of UIRefreshControl, resetting the native props to the default values.
// However, when recycling, we are keeping around the old _props. The flag is used to force the application
// of the current props to the newly created UIRefreshControl the first time that updateProps is called.
BOOL _recycled;
}
- (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();
_recycled = NO;
[self _initializeUIRefreshControl];
}
return self;
}
- (void)_initializeUIRefreshControl
{
_refreshControl = [UIRefreshControl new];
[_refreshControl addTarget:self
action:@selector(handleUIControlEventValueChanged)
forControlEvents:UIControlEventValueChanged];
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<PullToRefreshViewComponentDescriptor>();
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_scrollViewComponentView = nil;
[self _initializeUIRefreshControl];
_recycled = YES;
}
- (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 (_recycled || newConcreteProps.tintColor != oldConcreteProps.tintColor) {
_refreshControl.tintColor = RCTUIColorFromSharedColor(newConcreteProps.tintColor);
}
if (_recycled || newConcreteProps.progressViewOffset != oldConcreteProps.progressViewOffset) {
[self _updateProgressViewOffset:newConcreteProps.progressViewOffset];
}
BOOL needsUpdateTitle = NO;
if (_recycled || newConcreteProps.title != oldConcreteProps.title) {
needsUpdateTitle = YES;
}
if (_recycled || newConcreteProps.titleColor != oldConcreteProps.titleColor) {
needsUpdateTitle = YES;
}
[super updateProps:props oldProps:oldProps];
if (_recycled || needsUpdateTitle) {
[self _updateTitle];
}
// All prop updates must happen above the call to begin refreshing, or else _refreshControl will ignore the updates
if (_recycled || newConcreteProps.refreshing != oldConcreteProps.refreshing) {
if (newConcreteProps.refreshing) {
[self beginRefreshingProgrammatically];
} else {
[_refreshControl endRefreshing];
}
}
if (_recycled || newConcreteProps.zIndex != oldConcreteProps.zIndex) {
_refreshControl.layer.zPosition = newConcreteProps.zIndex.value_or(0);
}
_recycled = NO;
}
#pragma mark -
- (void)handleUIControlEventValueChanged
{
static_cast<const PullToRefreshViewEventEmitter &>(*_eventEmitter).onRefresh({});
}
- (void)_updateProgressViewOffset:(Float)progressViewOffset
{
_refreshControl.bounds = CGRectMake(
_refreshControl.bounds.origin.x,
-progressViewOffset,
_refreshControl.bounds.size.width,
_refreshControl.bounds.size.height);
}
- (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)layoutSubviews
{
[super layoutSubviews];
// Attempts to begin refreshing before the initial layout are ignored by _refreshControl. So if the control is
// refreshing when mounted, we need to call beginRefreshing in layoutSubviews or it won't work.
if (self.window) {
const auto &concreteProps = static_cast<const PullToRefreshViewProps &>(*_props);
if (concreteProps.refreshing) {
[self beginRefreshingProgrammatically];
}
}
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
if (self.superview) {
[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;
// This ensures that layoutSubviews is called. Without this, recycled instances won't refresh on mount
[self setNeedsLayout];
}
}
- (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;
}
- (void)beginRefreshingProgrammatically
{
if (!_scrollViewComponentView) {
return;
}
// When refreshing programmatically (i.e. without pulling down), we must explicitly adjust the ScrollView content
// offset, or else the _refreshControl won't be visible
if (!_refreshControl.isRefreshing) {
UIScrollView *scrollView = _scrollViewComponentView.scrollView;
CGPoint offset = {scrollView.contentOffset.x, scrollView.contentOffset.y - _refreshControl.frame.size.height};
[scrollView setContentOffset:offset];
[_refreshControl beginRefreshing];
}
}
#pragma mark - Native commands
- (void)handleCommand:(const NSString *)commandName args:(const NSArray *)args
{
RCTPullToRefreshViewHandleCommand(self, commandName, args);
}
- (void)setNativeRefreshing:(BOOL)refreshing
{
if (refreshing) {
[self beginRefreshingProgrammatically];
} 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,77 @@
/*
* 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>
#import "RCTVirtualViewContainerProtocol.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, RCTVirtualViewContainerProtocol>
/*
* 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;
/** Focus area of newly-activated text input relative to the window to compare against UIKeyboardFrameBegin/End */
@property (nonatomic, assign) CGRect firstResponderFocus;
/** newly-activated text input outside of the scroll view */
@property (nonatomic, weak) UIView *firstResponderViewOutsideScrollView;
/*
* 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
@interface UIView (RCTScrollViewComponentView)
- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollViewComponentView *)scrollView;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
/*
* 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.
*/
@class RCTVirtualViewContainerState;
@protocol RCTVirtualViewContainerProtocol
- (RCTVirtualViewContainerState *)virtualViewContainerState;
@end

View File

@@ -0,0 +1,25 @@
/*
* 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 "RCTVirtualViewProtocol.h"
@class RCTScrollViewComponentView;
NS_ASSUME_NONNULL_BEGIN
@interface RCTVirtualViewContainerState : NSObject
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)new NS_UNAVAILABLE;
- (instancetype)initWithScrollView:(RCTScrollViewComponentView *)scrollView NS_DESIGNATED_INITIALIZER;
- (void)onChange:(id<RCTVirtualViewProtocol>)virtualView;
- (void)remove:(id<RCTVirtualViewProtocol>)virtualView;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,190 @@
/*
* 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/RCTLog.h>
#import <React/RCTScrollViewComponentView.h>
#import <React/RCTVirtualViewMode.h>
#import <UIKit/UIKit.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import "RCTVirtualViewContainerState.h"
using namespace facebook;
using namespace facebook::react;
#if RCT_DEBUG
static void debugLog(NSString *msg, ...)
{
auto debugEnabled = ReactNativeFeatureFlags::enableVirtualViewDebugFeatures();
if (!debugEnabled) {
return;
}
va_list args;
va_start(args, msg);
NSString *msgString = [[NSString alloc] initWithFormat:msg arguments:args];
RCTLogInfo(@"%@", msgString);
va_end(args); // Don't forget to call va_end to clean up
}
#endif
/**
* Checks whether one CGRect overlaps with another CGRect.
*
* This is different from CGRectIntersectsRect because a CGRect representing
* a line or a point is considered to overlap with another CGRect if the line
* or point is within the rect bounds. However, two CGRects are not considered
* to overlap if they only share a boundary.
*/
static BOOL CGRectOverlaps(CGRect rect1, CGRect rect2)
{
CGFloat minY1 = CGRectGetMinY(rect1);
CGFloat maxY1 = CGRectGetMaxY(rect1);
CGFloat minY2 = CGRectGetMinY(rect2);
CGFloat maxY2 = CGRectGetMaxY(rect2);
if (minY1 >= maxY2 || minY2 >= maxY1) {
// No overlap on the y-axis.
return NO;
}
CGFloat minX1 = CGRectGetMinX(rect1);
CGFloat maxX1 = CGRectGetMaxX(rect1);
CGFloat minX2 = CGRectGetMinX(rect2);
CGFloat maxX2 = CGRectGetMaxX(rect2);
if (minX1 >= maxX2 || minX2 >= maxX1) {
// No overlap on the x-axis.
return NO;
}
return YES;
}
@interface RCTVirtualViewContainerState () <UIScrollViewDelegate>
@end
@interface RCTVirtualViewContainerState () {
NSMutableSet<id<RCTVirtualViewProtocol>> *_virtualViews;
CGRect _emptyRect;
CGRect _prerenderRect;
__weak RCTScrollViewComponentView *_scrollViewComponentView;
CGFloat _prerenderRatio;
}
@end
@implementation RCTVirtualViewContainerState
- (instancetype)initWithScrollView:(RCTScrollViewComponentView *)scrollView
{
self = [super init];
if (self != nil) {
_virtualViews = [NSMutableSet set];
_emptyRect = CGRectZero;
_prerenderRect = CGRectZero;
_scrollViewComponentView = scrollView;
_prerenderRatio = ReactNativeFeatureFlags::virtualViewPrerenderRatio();
[_scrollViewComponentView addScrollListener:self];
#if RCT_DEBUG
debugLog(@"initWithScrollView");
#endif
}
return self;
}
- (void)dealloc
{
#if RCT_DEBUG
debugLog(@"dealloc");
#endif
if (_scrollViewComponentView != nil) {
[_scrollViewComponentView removeScrollListener:self];
_scrollViewComponentView = nil;
}
[_virtualViews removeAllObjects];
}
#pragma mark - Public API
- (void)onChange:(id<RCTVirtualViewProtocol>)virtualView
{
if (![_virtualViews containsObject:virtualView]) {
[_virtualViews addObject:virtualView];
#if RCT_DEBUG
debugLog(@"Add virtualViewID=%@", virtualView.virtualViewID);
#endif
} else {
#if RCT_DEBUG
debugLog(@"Update virtualViewID=%@", virtualView.virtualViewID);
#endif
}
[self _updateModes:virtualView];
}
- (void)remove:(id<RCTVirtualViewProtocol>)virtualView
{
if (![_virtualViews containsObject:virtualView]) {
RCTLogError(@"Attempting to remove non-existent VirtualView: %@", virtualView.virtualViewID);
}
[_virtualViews removeObject:virtualView];
#if RCT_DEBUG
debugLog(@"Remove virtualViewID=%@", virtualView.virtualViewID);
#endif
}
#pragma mark - Private Helpers
- (void)_updateModes:(id<RCTVirtualViewProtocol>)virtualView
{
auto scrollView = _scrollViewComponentView.scrollView;
CGRect visibleRect = CGRectMake(
scrollView.contentOffset.x,
scrollView.contentOffset.y,
scrollView.frame.size.width,
scrollView.frame.size.height);
_prerenderRect = visibleRect;
_prerenderRect = CGRectInset(
_prerenderRect, -_prerenderRect.size.width * _prerenderRatio, -_prerenderRect.size.height * _prerenderRatio);
NSArray<id<RCTVirtualViewProtocol>> *virtualViewsIt =
(virtualView != nullptr) ? @[ virtualView ] : [_virtualViews allObjects];
for (id<RCTVirtualViewProtocol> vv = nullptr in virtualViewsIt) {
CGRect rect = [vv containerRelativeRect:scrollView];
RCTVirtualViewMode mode = RCTVirtualViewModeHidden;
CGRect thresholdRect = _emptyRect;
if (CGRectOverlaps(rect, visibleRect)) {
thresholdRect = visibleRect;
mode = RCTVirtualViewModeVisible;
} else if (CGRectOverlaps(rect, _prerenderRect)) {
mode = RCTVirtualViewModePrerender;
thresholdRect = _prerenderRect;
}
#if RCT_DEBUG
debugLog(
@"UpdateModes virtualView=%@ mode=%ld rect=%@ thresholdRect=%@",
vv.virtualViewID,
(long)mode,
NSStringFromCGRect(rect),
NSStringFromCGRect(thresholdRect));
#endif
[vv onModeChange:mode targetRect:rect thresholdRect:thresholdRect];
}
}
#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self _updateModes:nil];
}
@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 <React/RCTVirtualViewMode.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol RCTVirtualViewProtocol <NSObject>
- (NSString *)virtualViewID;
- (CGRect)containerRelativeRect:(UIView *)view;
- (void)onModeChange:(RCTVirtualViewMode)newMode targetRect:(CGRect)targetRect thresholdRect:(CGRect)thresholdRect;
@end
NS_ASSUME_NONNULL_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 <Switch> component.
*/
@interface RCTSwitchComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,133 @@
/*
* 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/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/Props.h>
#import <react/renderer/components/FBReactNativeSpec/RCTComponentViewHelpers.h>
#import <react/renderer/components/switch/AppleSwitchComponentDescriptor.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 (!_isInitialValueSet || oldSwitchProps.value != newSwitchProps.value) {
BOOL shouldAnimate = _isInitialValueSet == YES;
[_switchView setOn:newSwitchProps.value animated:shouldAnimate];
}
// `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);
}
_isInitialValueSet = YES;
[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)});
}
// UISwitch is the accessibility element not this view. If this is YES we block
// accessibility on the switch itself
- (BOOL)isAccessibilityElement
{
return NO;
}
- (NSObject *)accessibilityElement
{
return _switchView;
}
#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 frameInContainerSpace;
@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(_frameInContainerSpace, CGRectZero)) {
return UIAccessibilityConvertFrameToScreenCoordinates(container.bounds, container);
} else {
return UIAccessibilityConvertFrameToScreenCoordinates(_frameInContainerSpace, 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,186 @@
/*
* 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];
firstElement.isAccessibilityElement = YES;
firstElement.accessibilityTraits = _view.accessibilityTraits;
firstElement.accessibilityLabel = accessibilityLabel;
firstElement.accessibilityLanguage = _view.accessibilityLanguage;
[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 ((![value isEqualToString:@"button"] && ![value isEqualToString:@"link"])) {
return;
}
if ([fragmentText isEqualToString:firstElement.accessibilityLabel]) {
if ([value isEqualToString:@"link"]) {
firstElement.accessibilityTraits |= UIAccessibilityTraitLink;
} else if ([value isEqualToString:@"button"]) {
firstElement.accessibilityTraits |= UIAccessibilityTraitButton;
}
// The fragment is the entire paragraph. This is handled as `firstElement`.
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.frameInContainerSpace = 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,413 @@
/*
* 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;
// ParagraphTextView is an auxiliary view we set as contentView so the drawing
// can happen on top of the layers manipulated by RCTViewComponentView (the parent view)
@interface RCTParagraphTextView : UIView
@property (nonatomic) ParagraphShadowNode::ConcreteState::Shared state;
@property (nonatomic) ParagraphAttributes paragraphAttributes;
@property (nonatomic) LayoutMetrics layoutMetrics;
@end
@interface RCTParagraphComponentView () <UIEditMenuInteractionDelegate>
@property (nonatomic, nullable) UIEditMenuInteraction *editMenuInteraction API_AVAILABLE(ios(16.0));
@end
@implementation RCTParagraphComponentView {
ParagraphAttributes _paragraphAttributes;
RCTParagraphComponentAccessibilityProvider *_accessibilityProvider;
UILongPressGestureRecognizer *_longPressGestureRecognizer;
RCTParagraphTextView *_textView;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ParagraphShadowNode::defaultSharedProps();
self.opaque = NO;
_textView = [RCTParagraphTextView new];
_textView.backgroundColor = UIColor.clearColor;
self.contentView = _textView;
}
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 (!_textView.state) {
return nil;
}
return RCTNSAttributedStringFromAttributedString(_textView.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;
_textView.paragraphAttributes = _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
{
_textView.state = std::static_pointer_cast<const ParagraphShadowNode::ConcreteState>(state);
[_textView setNeedsDisplay];
[self setNeedsLayout];
}
- (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];
_textView.layoutMetrics = _layoutMetrics;
[_textView setNeedsDisplay];
[self setNeedsLayout];
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_textView.state = nullptr;
_accessibilityProvider = nil;
}
- (void)layoutSubviews
{
[super layoutSubviews];
_textView.frame = self.bounds;
}
#pragma mark - Accessibility
- (NSString *)accessibilityLabel
{
NSString *label = super.accessibilityLabel;
if ([label length] > 0) {
return label;
}
return self.attributedText.string;
}
- (NSString *)accessibilityLabelForCoopting
{
return self.accessibilityLabel;
}
- (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 (!_textView.state || !paragraphProps.accessible) {
return [NSArray new];
}
auto &data = _textView.state->getData();
if (![_accessibilityProvider isUpToDate:data.attributedString]) {
auto textLayoutManager = data.layoutManager.lock();
if (textLayoutManager) {
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];
}
}
NSArray<UIAccessibilityElement *> *elements = _accessibilityProvider.accessibilityElements;
if ([elements count] > 0) {
elements[0].isAccessibilityElement =
elements[0].accessibilityTraits & UIAccessibilityTraitLink || ![self isAccessibilityCoopted];
}
return elements;
}
- (BOOL)isAccessibilityCoopted
{
UIView *ancestor = self.superview;
NSMutableSet<UIView *> *cooptingCandidates = [NSMutableSet new];
while (ancestor) {
if ([ancestor isKindOfClass:[RCTViewComponentView class]]) {
if ([((RCTViewComponentView *)ancestor) accessibilityLabelForCoopting]) {
// We found a label above us. That would be coopted before we would be
return NO;
} else if ([((RCTViewComponentView *)ancestor) wantsToCooptLabel]) {
// We found an view that is looking to coopt a label below it
[cooptingCandidates addObject:ancestor];
}
NSArray *elements = ancestor.accessibilityElements;
if ([elements count] > 0 && [cooptingCandidates count] > 0) {
for (NSObject *element in elements) {
if ([element isKindOfClass:[UIView class]] && [cooptingCandidates containsObject:((UIView *)element)]) {
return YES;
}
}
}
} else if (![ancestor isKindOfClass:[RCTViewComponentView class]] && ancestor.accessibilityLabel) {
// Same as above, for UIView case. Cannot call this on RCTViewComponentView
// as it is recursive and quite expensive.
return NO;
}
ancestor = ancestor.superview;
}
return NO;
}
- (UIAccessibilityTraits)accessibilityTraits
{
return [super accessibilityTraits] | UIAccessibilityTraitStaticText;
}
#pragma mark - RCTTouchableComponentViewProtocol
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
{
const auto &state = _textView.state;
if (!state) {
return _eventEmitter;
}
const auto &stateData = state->getData();
auto textLayoutManager = stateData.layoutManager.lock();
if (!textLayoutManager) {
return _eventEmitter;
}
RCTTextLayoutManager *nativeTextLayoutManager =
(RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager());
CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
auto eventEmitter = [nativeTextLayoutManager getEventEmitterWithAttributeString:stateData.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;
}
@implementation RCTParagraphTextView {
CAShapeLayer *_highlightLayer;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return nil;
}
- (void)drawRect:(CGRect)rect
{
if (!_state) {
return;
}
const auto &stateData = _state->getData();
auto textLayoutManager = stateData.layoutManager.lock();
if (!textLayoutManager) {
return;
}
RCTTextLayoutManager *nativeTextLayoutManager =
(RCTTextLayoutManager *)unwrapManagedObject(textLayoutManager->getNativeTextLayoutManager());
CGRect frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
[nativeTextLayoutManager drawAttributedString:stateData.attributedString
paragraphAttributes:_paragraphAttributes
frame:frame
drawHighlightPath:^(UIBezierPath *highlightPath) {
if (highlightPath) {
if (!self->_highlightLayer) {
self->_highlightLayer = [CAShapeLayer layer];
self->_highlightLayer.fillColor = [UIColor colorWithWhite:0 alpha:0.25].CGColor;
[self.layer addSublayer:self->_highlightLayer];
}
self->_highlightLayer.position = frame.origin;
self->_highlightLayer.path = highlightPath.CGPath;
} else {
[self->_highlightLayer removeFromSuperlayer];
self->_highlightLayer = nil;
}
}];
}
@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 <TextInput> component.
*/
@interface RCTTextInputComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,889 @@
/*
* 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/featureflags/ReactNativeFeatureFlags.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/RCTScrollViewComponentView.h>
#import <React/RCTUITextField.h>
#import <React/RCTUITextView.h>
#import <React/RCTUtils.h>
#import "RCTConversions.h"
#import "RCTTextInputNativeCommands.h"
#import "RCTTextInputUtils.h"
#import <limits>
#import "RCTFabricComponentsPlugins.h"
/** Native iOS text field bottom keyboard offset amount */
static const CGFloat kSingleLineKeyboardBottomOffset = 15.0;
using namespace facebook::react;
@interface RCTTextInputComponentView () <
RCTBackedTextInputDelegate,
RCTTextInputViewProtocol,
UIDropInteractionDelegate>
@end
static NSSet<NSNumber *> *returnKeyTypesSet;
@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;
/*
* Newly initialized default typing attributes contain a no-op NSParagraphStyle and NSShadow. These cause inequality
* between the AttributedString backing the input and those generated from state. We store these attributes to make
* later comparison insensitive to them.
*/
NSDictionary<NSAttributedStringKey, id> *_originalTypingAttributes;
BOOL _hasInputAccessoryView;
CGSize _previousContentSize;
}
#pragma mark - UIView overrides
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
const auto &defaultProps = TextInputShadowNode::defaultSharedProps();
_props = defaultProps;
_backedTextInputView = defaultProps->multiline ? [RCTUITextView new] : [RCTUITextField new];
_backedTextInputView.textInputDelegate = self;
_ignoreNextTextInputCall = NO;
_comingFromJS = NO;
_didMoveToWindow = NO;
_originalTypingAttributes = [_backedTextInputView.typingAttributes copy];
_previousContentSize = CGSizeZero;
[self addSubview:_backedTextInputView];
[self initializeReturnKeyType];
}
return self;
}
- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
[_backedTextInputView.defaultTextAttributes mutableCopy];
defaultAttributes[RCTAttributedStringEventEmitterKey] = RCTWrapEventEmitter(_eventEmitter);
_backedTextInputView.defaultTextAttributes = defaultAttributes;
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (self.window && !_didMoveToWindow) {
const auto &props = static_cast<const TextInputProps &>(*_props);
if (props.autoFocus) {
[_backedTextInputView becomeFirstResponder];
[self scrollCursorIntoView];
}
_didMoveToWindow = YES;
[self initializeReturnKeyType];
}
[self _restoreTextSelection];
}
// TODO: replace with registerForTraitChanges once iOS 17.0 is the lowest supported version
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
if (facebook::react::ReactNativeFeatureFlags::enableFontScaleChangesUpdatingLayout() &&
UITraitCollection.currentTraitCollection.preferredContentSizeCategory !=
previousTraitCollection.preferredContentSizeCategory) {
const auto &newTextInputProps = static_cast<const TextInputProps &>(*_props);
_backedTextInputView.defaultTextAttributes =
RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
}
}
- (void)reactUpdateResponderOffsetForScrollView:(RCTScrollViewComponentView *)scrollView
{
if (![self isDescendantOfView:scrollView.scrollView] || !_backedTextInputView.isFirstResponder) {
// View is outside scroll view or it's not a first responder.
scrollView.firstResponderViewOutsideScrollView = _backedTextInputView;
return;
}
UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange;
UITextSelectionRect *selection = [_backedTextInputView selectionRectsForRange:selectedTextRange].firstObject;
CGRect focusRect;
if (selection == nil) {
// No active selection or caret - fallback to entire input frame
focusRect = self.bounds;
} else {
// Focus on text selection frame
focusRect = selection.rect;
BOOL isMultiline = [_backedTextInputView isKindOfClass:[UITextView class]];
if (!isMultiline) {
focusRect.size.height += kSingleLineKeyboardBottomOffset;
}
}
scrollView.firstResponderFocus = [self convertRect:focusRect toView:nil];
}
#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.multiline != oldTextInputProps.multiline) {
[self _setMultiline:newTextInputProps.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.multiline &&
newTextInputProps.traits.dataDetectorTypes != oldTextInputProps.traits.dataDetectorTypes) {
_backedTextInputView.dataDetectorTypes =
RCTUITextViewDataDetectorTypesFromStringVector(newTextInputProps.traits.dataDetectorTypes);
}
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);
}
if (newTextInputProps.traits.showSoftInputOnFocus != oldTextInputProps.traits.showSoftInputOnFocus) {
[self _setShowSoftInputOnFocus:newTextInputProps.traits.showSoftInputOnFocus];
}
// 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) {
NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
defaultAttributes[RCTAttributedStringEventEmitterKey] =
_backedTextInputView.defaultTextAttributes[RCTAttributedStringEventEmitterKey];
_backedTextInputView.defaultTextAttributes = defaultAttributes;
}
if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) {
_backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor);
}
if (newTextInputProps.inputAccessoryViewID != oldTextInputProps.inputAccessoryViewID) {
_backedTextInputView.inputAccessoryViewID = RCTNSStringFromString(newTextInputProps.inputAccessoryViewID);
}
if (newTextInputProps.inputAccessoryViewButtonLabel != oldTextInputProps.inputAccessoryViewButtonLabel) {
_backedTextInputView.inputAccessoryViewButtonLabel =
RCTNSStringFromString(newTextInputProps.inputAccessoryViewButtonLabel);
}
if (newTextInputProps.disableKeyboardShortcuts != oldTextInputProps.disableKeyboardShortcuts) {
_backedTextInputView.disableKeyboardShortcuts = newTextInputProps.disableKeyboardShortcuts;
}
if (newTextInputProps.acceptDragAndDropTypes != oldTextInputProps.acceptDragAndDropTypes) {
if (!newTextInputProps.acceptDragAndDropTypes.has_value()) {
_backedTextInputView.acceptDragAndDropTypes = nil;
} else {
auto &vector = newTextInputProps.acceptDragAndDropTypes.value();
NSMutableArray<NSString *> *array = [NSMutableArray arrayWithCapacity:vector.size()];
for (const std::string &str : vector) {
[array addObject:[NSString stringWithUTF8String:str.c_str()]];
}
_backedTextInputView.acceptDragAndDropTypes = array;
}
}
[super updateProps:props oldProps:oldProps];
[self setDefaultInputAccessoryView];
}
- (void)updateState:(const State::Shared &)state oldState:(const State::Shared &)oldState
{
_state = std::static_pointer_cast<const TextInputShadowNode::ConcreteState>(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 (!CGSizeEqualToSize(_previousContentSize, _backedTextInputView.contentSize) && _eventEmitter) {
_previousContentSize = _backedTextInputView.contentSize;
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.inputAccessoryViewID = nil;
_backedTextInputView.inputAccessoryView = nil;
_hasInputAccessoryView = false;
[_backedTextInputView resignFirstResponder];
}
#pragma mark - RCTBackedTextInputDelegate
- (BOOL)textInputShouldBeginEditing
{
return YES;
}
- (void)textInputDidBeginEditing
{
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) {
const auto &textInputEventEmitter = static_cast<const TextInputEventEmitter &>(*_eventEmitter);
textInputEventEmitter.onKeyPress({
.text = RCTStringFromNSString(text),
.eventCount = static_cast<int>(_mostRecentEventCount),
});
}
}
if (props.maxLength < std::numeric_limits<int>::max()) {
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);
textInputEventEmitter.onChange([self _textInputMetrics]);
}
}
- (void)textInputDidChangeSelection
{
if (_comingFromJS) {
return;
}
// T207198334: Setting a new AttributedString (_comingFromJS) will trigger a selection change before the backing
// string is updated, so indicies won't point to what we want yet. Only respond to user selection change, and let
// `_setAttributedString` handle updating typing attributes if content changes.
[self _updateTypingAttributes];
const auto &props = static_cast<const TextInputProps &>(*_props);
if (props.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];
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];
}
[self scrollCursorIntoView];
}
- (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];
// ensure we scroll to the selected position
NSInteger offsetEnd = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
toPosition:range.end];
[_backedTextInputView scrollRangeToVisible:NSMakeRange(offsetEnd, 0)];
}
_comingFromJS = NO;
}
#pragma mark - Default input accessory view
- (NSString *)returnKeyTypeToString:(UIReturnKeyType)returnKeyType
{
switch (returnKeyType) {
case UIReturnKeyGo:
return @"Go";
case UIReturnKeyNext:
return @"Next";
case UIReturnKeySearch:
return @"Search";
case UIReturnKeySend:
return @"Send";
case UIReturnKeyYahoo:
return @"Yahoo";
case UIReturnKeyGoogle:
return @"Google";
case UIReturnKeyRoute:
return @"Route";
case UIReturnKeyJoin:
return @"Join";
case UIReturnKeyEmergencyCall:
return @"Emergency Call";
default:
return @"Done";
}
}
- (void)initializeReturnKeyType
{
returnKeyTypesSet = [NSSet setWithObjects:@(UIReturnKeyDone),
@(UIReturnKeyGo),
@(UIReturnKeyNext),
@(UIReturnKeySearch),
@(UIReturnKeySend),
@(UIReturnKeyYahoo),
@(UIReturnKeyGoogle),
@(UIReturnKeyRoute),
@(UIReturnKeyJoin),
@(UIReturnKeyRoute),
@(UIReturnKeyEmergencyCall),
nil];
}
- (void)setDefaultInputAccessoryView
{
// InputAccessoryView component sets the inputAccessoryView when inputAccessoryViewID exists
if (_backedTextInputView.inputAccessoryViewID) {
if (_backedTextInputView.isFirstResponder) {
[_backedTextInputView reloadInputViews];
}
return;
}
UIKeyboardType keyboardType = _backedTextInputView.keyboardType;
UIReturnKeyType returnKeyType = _backedTextInputView.returnKeyType;
NSString *inputAccessoryViewButtonLabel = _backedTextInputView.inputAccessoryViewButtonLabel;
BOOL containsKeyType = [returnKeyTypesSet containsObject:@(returnKeyType)];
BOOL containsInputAccessoryViewButtonLabel = inputAccessoryViewButtonLabel != nil;
// These keyboard types (all are number pads) don't have a "returnKey" button by default,
// so we create an `inputAccessoryView` with this button for them.
BOOL shouldHaveInputAccessoryView =
(keyboardType == UIKeyboardTypeNumberPad || keyboardType == UIKeyboardTypePhonePad ||
keyboardType == UIKeyboardTypeDecimalPad || keyboardType == UIKeyboardTypeASCIICapableNumberPad) &&
(containsKeyType || containsInputAccessoryViewButtonLabel);
if (_hasInputAccessoryView == shouldHaveInputAccessoryView) {
return;
}
_hasInputAccessoryView = shouldHaveInputAccessoryView;
if (shouldHaveInputAccessoryView) {
NSString *buttonLabel = inputAccessoryViewButtonLabel != nil ? inputAccessoryViewButtonLabel
: [self returnKeyTypeToString:returnKeyType];
UIToolbar *toolbarView = [UIToolbar new];
[toolbarView sizeToFit];
UIBarButtonItem *flexibleSpace =
[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:buttonLabel
style:UIBarButtonItemStylePlain
target:self
action:@selector(handleInputAccessoryDoneButton)];
toolbarView.items = @[ flexibleSpace, doneButton ];
_backedTextInputView.inputAccessoryView = toolbarView;
} else {
_backedTextInputView.inputAccessoryView = nil;
}
if (_backedTextInputView.isFirstResponder) {
[_backedTextInputView reloadInputViews];
}
}
- (void)handleInputAccessoryDoneButton
{
// Ignore the value of whether we submitted; just make sure the submit event is called if necessary.
[self textInputShouldSubmitOnReturn];
if ([self textInputShouldReturn]) {
[_backedTextInputView endEditing:YES];
}
}
#pragma mark - Other
- (TextInputEventEmitter::Metrics)_textInputMetrics
{
return {
.text = RCTStringFromNSString(_backedTextInputView.attributedText.string),
.selectionRange = [self _selectionRange],
.eventCount = static_cast<int>(_mostRecentEventCount),
.contentOffset = RCTPointFromCGPoint(_backedTextInputView.contentOffset),
.contentInset = RCTEdgeInsetsFromUIEdgeInsets(_backedTextInputView.contentInset),
.contentSize = RCTSizeFromCGSize(_backedTextInputView.contentSize),
.layoutMeasurement = RCTSizeFromCGSize(_backedTextInputView.bounds.size),
.zoomScale = _backedTextInputView.zoomScale,
};
}
- (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{.location = (int)start, .length = (int)(end - start)};
}
- (void)_restoreTextSelection
{
[self _restoreTextSelectionAndIgnoreCaretChange:NO];
}
- (void)_restoreTextSelectionAndIgnoreCaretChange:(BOOL)ignore
{
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];
if (ignore && range.empty) {
return;
}
[_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;
// Updating the UITextView attributedText, for example changing the lineHeight, the color or adding
// a new paragraph with \n, causes the cursor to move to the end of the Text and scroll.
// This is fixed by restoring the cursor position and scrolling to that position (iOS issue 652653).
// 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];
[_backedTextInputView scrollRangeToVisible:NSMakeRange(offsetStart, 0)];
// A zero-length selection range can cause the caret position to change on iOS,
// and we have already updated the caret position, so we can safely ignore caret changing in this place.
[self _restoreTextSelectionAndIgnoreCaretChange:YES];
[self _updateTypingAttributes];
_lastStringStateWasUpdatedWith = attributedString;
}
// Ensure that newly typed text will inherit any custom attributes. We follow the logic of RN Android, where attributes
// to the left of the cursor are copied into new text, unless we are at the start of the field, in which case we will
// copy the attributes from text to the right. This allows consistency between backed input and new AttributedText
// https://github.com/facebook/react-native/blob/3102a58df38d96f3dacef0530e4dbb399037fcd2/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/internal/span/SetSpanOperation.kt#L30
- (void)_updateTypingAttributes
{
if (_backedTextInputView.attributedText.length > 0 && _backedTextInputView.selectedTextRange != nil) {
NSUInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
toPosition:_backedTextInputView.selectedTextRange.start];
NSUInteger samplePoint = offsetStart == 0 ? 0 : offsetStart - 1;
_backedTextInputView.typingAttributes = [_backedTextInputView.attributedText attributesAtIndex:samplePoint
effectiveRange:NULL];
}
}
- (void)scrollCursorIntoView
{
UITextRange *selectedRange = _backedTextInputView.selectedTextRange;
if (selectedRange.empty) {
NSInteger offsetStart = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
toPosition:selectedRange.start];
[_backedTextInputView scrollRangeToVisible:NSMakeRange(offsetStart, 0)];
}
}
- (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];
}
- (void)_setShowSoftInputOnFocus:(BOOL)showSoftInputOnFocus
{
if (showSoftInputOnFocus) {
// Resets to default keyboard.
_backedTextInputView.inputView = nil;
// Without the call to reloadInputViews, the keyboard will not change until the textInput field (the first
// responder) loses and regains focus.
if (_backedTextInputView.isFirstResponder) {
[_backedTextInputView reloadInputViews];
}
} else {
// Hides keyboard, but keeps blinking cursor.
_backedTextInputView.inputView = [UIView new];
}
}
- (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 RCTIsAttributedStringEffectivelySame(
newText, oldText, _originalTypingAttributes, static_cast<const TextInputProps &>(*_props).textAttributes);
}
}
- (SubmitBehavior)getSubmitBehavior
{
const auto &props = static_cast<const TextInputProps &>(*_props);
return props.getNonDefaultSubmitBehavior();
}
@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,46 @@
/*
* 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);
UIDataDetectorTypes RCTUITextViewDataDetectorTypesFromStringVector(const std::vector<std::string> &dataDetectorTypes);
NS_ASSUME_NONNULL_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 "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.disableKeyboardShortcuts = fromTextInput.disableKeyboardShortcuts;
toTextInput.acceptDragAndDropTypes = fromTextInput.acceptDragAndDropTypes;
[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 (@available(iOS 15.0, *)) {
[mutableContentTypeMap addEntriesFromDictionary:@{
@"dateTime" : UITextContentTypeDateTime,
@"flightNumber" : UITextContentTypeFlightNumber,
@"shipmentTrackingNumber" : UITextContentTypeShipmentTrackingNumber,
}];
}
#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,
}];
}
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 170400 /* __IPHONE_17_4 */
if (@available(iOS 17.4, *)) {
[mutableContentTypeMap addEntriesFromDictionary:@{
@"cellularEID" : UITextContentTypeCellularEID,
@"cellularIMEI" : UITextContentTypeCellularIMEI,
}];
}
#endif
#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;
}
UIDataDetectorTypes RCTUITextViewDataDetectorTypesFromStringVector(const std::vector<std::string> &dataDetectorTypes)
{
static dispatch_once_t onceToken;
static NSDictionary<NSString *, NSNumber *> *dataDetectorTypesMap = nil;
dispatch_once(&onceToken, ^{
dataDetectorTypesMap = @{
@"link" : @(UIDataDetectorTypeLink),
@"phoneNumber" : @(UIDataDetectorTypePhoneNumber),
@"address" : @(UIDataDetectorTypeAddress),
@"calendarEvent" : @(UIDataDetectorTypeCalendarEvent),
@"trackingNumber" : @(UIDataDetectorTypeShipmentTrackingNumber),
@"flightNumber" : @(UIDataDetectorTypeFlightNumber),
@"lookupSuggestion" : @(UIDataDetectorTypeLookupSuggestion),
@"all" : @(UIDataDetectorTypeAll)
};
});
UIDataDetectorTypes ret = UIDataDetectorTypeNone;
for (const auto &dataType : dataDetectorTypes) {
NSNumber *val = dataDetectorTypesMap[RCTNSStringFromString(dataType)];
if (val) {
ret |= (UIDataDetectorTypes)val.unsignedIntValue;
}
}
return ret;
}

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/FBReactNativeSpec/ComponentDescriptors.h>
#import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/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/FBReactNativeSpec/ComponentDescriptors.h>
#import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/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,96 @@
/*
* 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;
- (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event;
/*
* This is the label that would be coopted by another element
*/
- (NSString *)accessibilityLabelForCoopting;
/*
* This View has no label and will look to coopt something below it
*/
- (BOOL)wantsToCooptLabel;
/*
* 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

File diff suppressed because it is too large Load Diff

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/RCTViewComponentView.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTVirtualViewComponentView : RCTViewComponentView
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,387 @@
/*
* 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 "RCTVirtualViewComponentView.h"
#import <React/RCTAssert.h>
#import <React/RCTConversions.h>
#import <React/RCTScrollViewComponentView.h>
#import <React/RCTScrollableProtocol.h>
#import <React/UIView+React.h>
#import <jsi/jsi.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/FBReactNativeSpec/ComponentDescriptors.h>
#import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/Props.h>
#import <react/renderer/components/virtualview/VirtualViewComponentDescriptor.h>
#import <react/renderer/components/virtualview/VirtualViewShadowNode.h>
#import "../VirtualViewExperimental/RCTVirtualViewMode.h"
#import "../VirtualViewExperimental/RCTVirtualViewRenderState.h"
#import "RCTFabricComponentsPlugins.h"
using namespace facebook;
using namespace facebook::react;
/**
* Checks whether one CGRect overlaps with another CGRect.
*
* This is different from CGRectIntersectsRect because a CGRect representing
* a line or a point is considered to overlap with another CGRect if the line
* or point is within the rect bounds. However, two CGRects are not considered
* to overlap if they only share a boundary.
*/
static BOOL CGRectOverlaps(CGRect rect1, CGRect rect2)
{
CGFloat minY1 = CGRectGetMinY(rect1);
CGFloat maxY1 = CGRectGetMaxY(rect1);
CGFloat minY2 = CGRectGetMinY(rect2);
CGFloat maxY2 = CGRectGetMaxY(rect2);
if (minY1 >= maxY2 || minY2 >= maxY1) {
// No overlap on the y-axis.
return NO;
}
CGFloat minX1 = CGRectGetMinX(rect1);
CGFloat maxX1 = CGRectGetMaxX(rect1);
CGFloat minX2 = CGRectGetMinX(rect2);
CGFloat maxX2 = CGRectGetMaxX(rect2);
if (minX1 >= maxX2 || minX2 >= maxX1) {
// No overlap on the x-axis.
return NO;
}
return YES;
}
@interface RCTVirtualViewComponentView () <UIScrollViewDelegate>
@end
@implementation RCTVirtualViewComponentView {
RCTScrollViewComponentView *_lastParentScrollViewComponentView;
std::optional<RCTVirtualViewMode> _mode;
RCTVirtualViewRenderState _renderState;
std::optional<CGRect> _targetRect;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = VirtualViewShadowNode::defaultSharedProps();
_renderState = RCTVirtualViewRenderStateUnknown;
}
return self;
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &newViewProps = static_cast<const VirtualViewProps &>(*props);
if (!_mode.has_value()) {
_mode = newViewProps.initialHidden ? RCTVirtualViewModeHidden : RCTVirtualViewModeVisible;
if (ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS()) {
self.hidden = newViewProps.initialHidden && !sIsAccessibilityUsed;
}
}
// If disabled, `_renderState` will always be `RCTVirtualViewRenderStateUnknown`.
if (ReactNativeFeatureFlags::enableVirtualViewRenderState()) {
switch (newViewProps.renderState) {
case 1:
_renderState = RCTVirtualViewRenderStateRendered;
break;
case 2:
_renderState = RCTVirtualViewRenderStateNone;
break;
default:
_renderState = RCTVirtualViewRenderStateUnknown;
break;
}
}
[super updateProps:props oldProps:oldProps];
}
- (RCTScrollViewComponentView *)getParentScrollViewComponentView
{
UIView *view = self.superview;
while (view != nil) {
if ([view isKindOfClass:[RCTScrollViewComponentView class]]) {
return (RCTScrollViewComponentView *)view;
}
view = view.superview;
}
return nil;
}
/**
* Static flag that tracks whether accessibility services are being used.
* When accessibility is detected, virtual views will remain visible even when
* they would normally be hidden when off-screen, ensuring that accessibility
* features will work correctly.
*/
static BOOL sIsAccessibilityUsed = NO;
- (void)_unhideIfNeeded
{
if (!sIsAccessibilityUsed) {
// accessibility is detected for the first time. Make views visible.
sIsAccessibilityUsed = YES;
}
if (self.hidden) {
self.hidden = NO;
}
}
- (NSInteger)accessibilityElementCount
{
// From empirical testing, method `accessibilityElementCount` is called lazily only
// when accessibility is used.
[self _unhideIfNeeded];
return [super accessibilityElementCount];
}
- (NSArray<id<UIFocusItem>> *)focusItemsInRect:(CGRect)rect
{
// From empirical testing, method `focusItemsInRect:` is called lazily only
// when keyboard navigation is used.
[self _unhideIfNeeded];
return [super focusItemsInRect:rect];
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
// No need to remove the scroll listener here since the view is always removed from window before being recycled and
// we do that in didMoveToWindow, which gets called when the view is removed from window.
RCTAssert(
_lastParentScrollViewComponentView == nil,
@"_lastParentScrollViewComponentView should already have been cleared in didMoveToWindow.");
self.hidden = NO;
_mode.reset();
_targetRect.reset();
}
// Handles case when sibling changes size.
// TODO(T202601695): This doesn't yet handle the case of elements in the ScrollView outside a VirtualColumn changing
// size.
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics];
[self dispatchOnModeChangeIfNeeded:YES];
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (_lastParentScrollViewComponentView) {
[_lastParentScrollViewComponentView removeScrollListener:self];
_lastParentScrollViewComponentView = nil;
}
if (RCTScrollViewComponentView *parentScrollViewComponentView = [self getParentScrollViewComponentView]) {
if (self.window) {
// TODO(T202601695): We also want the ScrollView to emit layout changes from didLayoutSubviews so that any event
// that may affect visibily of this view notifies the listeners.
[parentScrollViewComponentView addScrollListener:self];
_lastParentScrollViewComponentView = parentScrollViewComponentView;
// We want to dispatch the event immediately when the view is added to the window before any scrolling occurs.
[self dispatchOnModeChangeIfNeeded:NO];
}
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[self dispatchOnModeChangeIfNeeded:NO];
}
- (void)dispatchOnModeChangeIfNeeded:(BOOL)checkForTargetRectChange
{
if (!_lastParentScrollViewComponentView) {
return;
}
UIScrollView *scrollView = _lastParentScrollViewComponentView.scrollView;
CGRect targetRect = [self convertRect:self.bounds toView:scrollView];
// While scrolling, the `targetRect` does not change, so we don't check for changed `targetRect` in that case.
if (checkForTargetRectChange) {
if (_targetRect.has_value() && CGRectEqualToRect(targetRect, _targetRect.value())) {
return;
}
_targetRect = targetRect;
}
RCTVirtualViewMode newMode;
CGRect thresholdRect = CGRectMake(
scrollView.contentOffset.x,
scrollView.contentOffset.y,
scrollView.frame.size.width,
scrollView.frame.size.height);
const CGFloat visibleWidth = thresholdRect.size.width;
const CGFloat visibleHeight = thresholdRect.size.height;
if (CGRectOverlaps(targetRect, thresholdRect)) {
newMode = RCTVirtualViewModeVisible;
} else {
auto prerender = false;
const CGFloat prerenderRatio = ReactNativeFeatureFlags::virtualViewPrerenderRatio();
if (prerenderRatio > 0) {
thresholdRect = CGRectInset(thresholdRect, -visibleWidth * prerenderRatio, -visibleHeight * prerenderRatio);
prerender = CGRectOverlaps(targetRect, thresholdRect);
}
if (prerender) {
newMode = RCTVirtualViewModePrerender;
} else {
const CGFloat hysteresisRatio = ReactNativeFeatureFlags::virtualViewHysteresisRatio();
if (_mode.has_value() && hysteresisRatio > 0) {
thresholdRect = CGRectInset(thresholdRect, -visibleWidth * hysteresisRatio, -visibleHeight * hysteresisRatio);
if (CGRectOverlaps(targetRect, thresholdRect)) {
newMode = _mode.value();
} else {
newMode = RCTVirtualViewModeHidden;
thresholdRect = CGRectZero;
}
} else {
newMode = RCTVirtualViewModeHidden;
thresholdRect = CGRectZero;
}
}
}
if (_mode.has_value() && newMode == _mode.value()) {
return;
}
// NOTE: Make sure to keep these props in sync with dispatchSyncModeChange below where we have to explicitly copy all
// props.
VirtualViewEventEmitter::OnModeChange event = {
.mode = (int)newMode,
.targetRect =
{.x = targetRect.origin.x,
.y = targetRect.origin.y,
.width = targetRect.size.width,
.height = targetRect.size.height},
.thresholdRect =
{.x = thresholdRect.origin.x,
.y = thresholdRect.origin.y,
.width = thresholdRect.size.width,
.height = thresholdRect.size.height},
};
const std::optional<RCTVirtualViewMode> oldMode = _mode;
_mode = newMode;
switch (newMode) {
case RCTVirtualViewModeVisible:
if (_renderState == RCTVirtualViewRenderStateUnknown) {
// Feature flag is disabled, so use the former logic.
[self dispatchSyncModeChange:event];
} else {
// If the previous mode was prerender and the result of dispatching that event was committed, we do not need to
// dispatch an event for visible.
const auto wasPrerenderCommitted = oldMode.has_value() && oldMode == RCTVirtualViewModePrerender &&
_renderState == RCTVirtualViewRenderStateRendered;
if (!wasPrerenderCommitted) {
[self dispatchSyncModeChange:event];
}
}
break;
case RCTVirtualViewModePrerender:
if (!oldMode.has_value() || oldMode != RCTVirtualViewModeVisible) {
[self dispatchAsyncModeChange:event];
}
break;
case RCTVirtualViewModeHidden:
[self dispatchAsyncModeChange:event];
break;
}
if (ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS()) {
switch (newMode) {
case RCTVirtualViewModeVisible:
self.hidden = NO;
break;
case RCTVirtualViewModePrerender:
self.hidden = !sIsAccessibilityUsed;
break;
case RCTVirtualViewModeHidden:
self.hidden = YES;
break;
}
}
}
- (void)dispatchAsyncModeChange:(VirtualViewEventEmitter::OnModeChange &)event
{
if (!_eventEmitter) {
return;
}
std::shared_ptr<const VirtualViewEventEmitter> emitter =
std::static_pointer_cast<const VirtualViewEventEmitter>(_eventEmitter);
emitter->onModeChange(event);
}
- (void)dispatchSyncModeChange:(VirtualViewEventEmitter::OnModeChange &)event
{
if (!_eventEmitter) {
return;
}
std::shared_ptr<const VirtualViewEventEmitter> emitter =
std::static_pointer_cast<const VirtualViewEventEmitter>(_eventEmitter);
// TODO: Move this into a custom event emitter. We had to duplicate the codegen code here from onModeChange in order
// to dispatch synchronously and discrete.
emitter->experimental_flushSync([&emitter, &event]() {
emitter->dispatchEvent(
"modeChange",
[event](jsi::Runtime &runtime) {
auto payload = jsi::Object(runtime);
payload.setProperty(runtime, "mode", event.mode);
{
auto targetRect = jsi::Object(runtime);
targetRect.setProperty(runtime, "x", event.targetRect.x);
targetRect.setProperty(runtime, "y", event.targetRect.y);
targetRect.setProperty(runtime, "width", event.targetRect.width);
targetRect.setProperty(runtime, "height", event.targetRect.height);
payload.setProperty(runtime, "targetRect", targetRect);
}
{
auto thresholdRect = jsi::Object(runtime);
thresholdRect.setProperty(runtime, "x", event.thresholdRect.x);
thresholdRect.setProperty(runtime, "y", event.thresholdRect.y);
thresholdRect.setProperty(runtime, "width", event.thresholdRect.width);
thresholdRect.setProperty(runtime, "height", event.thresholdRect.height);
payload.setProperty(runtime, "thresholdRect", thresholdRect);
}
return payload;
},
RawEvent::Category::Discrete);
});
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<VirtualViewComponentDescriptor>();
}
@end
Class<RCTComponentViewProtocol> VirtualViewCls(void)
{
return RCTVirtualViewComponentView.class;
}

View File

@@ -0,0 +1,25 @@
/*
* 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/RCTVirtualViewProtocol.h>
NS_ASSUME_NONNULL_BEGIN
@interface RCTVirtualViewExperimentalComponentView : RCTViewComponentView <RCTVirtualViewProtocol>
+ (instancetype)new NS_UNAVAILABLE;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE;
- (instancetype)initWithFrame:(CGRect)frame NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,323 @@
/*
* 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 "RCTVirtualViewExperimentalComponentView.h"
#import <React/RCTAssert.h>
#import <React/RCTConversions.h>
#import <React/RCTScrollViewComponentView.h>
#import <React/RCTScrollableProtocol.h>
#import <React/RCTVirtualViewContainerProtocol.h>
#import <React/RCTVirtualViewContainerState.h>
#import <React/UIView+React.h>
#import <jsi/jsi.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/FBReactNativeSpec/ComponentDescriptors.h>
#import <react/renderer/components/FBReactNativeSpec/EventEmitters.h>
#import <react/renderer/components/FBReactNativeSpec/Props.h>
#import <react/renderer/components/virtualviewexperimental/VirtualViewExperimentalComponentDescriptor.h>
#import <react/renderer/components/virtualviewexperimental/VirtualViewExperimentalShadowNode.h>
#import "RCTFabricComponentsPlugins.h"
#import "RCTVirtualViewMode.h"
#import "RCTVirtualViewRenderState.h"
using namespace facebook;
using namespace facebook::react;
@interface RCTVirtualViewExperimentalComponentView () {
NSString *_virtualViewID;
}
@end
@implementation RCTVirtualViewExperimentalComponentView {
id<RCTVirtualViewContainerProtocol> _parentVirtualViewContainer;
std::optional<RCTVirtualViewMode> _mode;
RCTVirtualViewRenderState _renderState;
std::optional<CGRect> _targetRect;
NSString *_nativeId;
BOOL _didLayout;
}
#pragma mark - Public API
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]) != nil) {
_props = VirtualViewExperimentalShadowNode::defaultSharedProps();
_renderState = RCTVirtualViewRenderStateUnknown;
_virtualViewID = [[NSUUID UUID] UUIDString];
_didLayout = NO;
}
return self;
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
const auto &newViewProps = static_cast<const VirtualViewExperimentalProps &>(*props);
if (!_mode.has_value()) {
_mode = newViewProps.initialHidden ? RCTVirtualViewModeHidden : RCTVirtualViewModeVisible;
if (ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS()) {
self.hidden = newViewProps.initialHidden && !sIsAccessibilityUsed;
}
}
// If disabled, `_renderState` will always be `RCTVirtualViewRenderStateUnknown`.
if (ReactNativeFeatureFlags::enableVirtualViewRenderState()) {
switch (newViewProps.renderState) {
case 1:
_renderState = RCTVirtualViewRenderStateRendered;
break;
case 2:
_renderState = RCTVirtualViewRenderStateNone;
break;
default:
_renderState = RCTVirtualViewRenderStateUnknown;
break;
}
}
const auto &newBaseViewProps = static_cast<const ViewProps &>(*props);
const auto nativeId = RCTNSStringFromStringNilIfEmpty(newBaseViewProps.nativeId);
_virtualViewID = nativeId == nil ? _virtualViewID : nativeId;
[super updateProps:props oldProps:oldProps];
}
/**
* Static flag that tracks whether accessibility services are being used.
* When accessibility is detected, virtual views will remain visible even when
* they would normally be hidden when off-screen, ensuring that accessibility
* features will work correctly.
*/
static BOOL sIsAccessibilityUsed = NO;
- (NSInteger)accessibilityElementCount
{
// From empirical testing, method `accessibilityElementCount` is called lazily only
// when accessibility is used.
[self _unhideIfNeeded];
return [super accessibilityElementCount];
}
- (NSArray<id<UIFocusItem>> *)focusItemsInRect:(CGRect)rect
{
// From empirical testing, method `focusItemsInRect:` is called lazily only
// when keyboard navigation is used.
[self _unhideIfNeeded];
return [super focusItemsInRect:rect];
}
- (NSString *)virtualViewID
{
// Return a unique identifier for this virtual view
// Using the tag as a unique identifier since it's already unique per view
return _virtualViewID;
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
[[_parentVirtualViewContainer virtualViewContainerState] remove:self];
self.hidden = NO;
_didLayout = NO;
_mode.reset();
_targetRect.reset();
_parentVirtualViewContainer = nil;
}
// Handles case when sibling changes size.
// TODO(T202601695): This doesn't yet handle the case of elements in the ScrollView outside a VirtualColumn changing
// size.
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics];
_didLayout = YES;
[self updateState];
}
- (void)updateState
{
[[_parentVirtualViewContainer virtualViewContainerState] onChange:self];
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
// here we will set the pointer to the virtualView container
// and if there was a layout, update
_parentVirtualViewContainer = [self _getParentVirtualViewContainer];
if (_parentVirtualViewContainer != nil && self.window != nil && _didLayout) {
[self updateState];
}
}
- (CGRect)containerRelativeRect:(UIView *)scrollView
{
// Return the view's position relative to its container (the scroll view)
return [self convertRect:self.bounds toView:scrollView];
}
- (void)onModeChange:(RCTVirtualViewMode)newMode targetRect:(CGRect)targetRect thresholdRect:(CGRect)thresholdRect
{
if (_mode.has_value() && newMode == _mode.value()) {
return;
}
// NOTE: Make sure to keep these props in sync with dispatchSyncModeChange below where we have to explicitly copy
// all props.
VirtualViewEventEmitter::OnModeChange event = {
.mode = (int)newMode,
.targetRect =
{.x = targetRect.origin.x,
.y = targetRect.origin.y,
.width = targetRect.size.width,
.height = targetRect.size.height},
.thresholdRect =
{.x = thresholdRect.origin.x,
.y = thresholdRect.origin.y,
.width = thresholdRect.size.width,
.height = thresholdRect.size.height},
};
const std::optional<RCTVirtualViewMode> oldMode = _mode;
_mode = newMode;
switch (newMode) {
case RCTVirtualViewModeVisible:
if (_renderState == RCTVirtualViewRenderStateUnknown) {
// Feature flag is disabled, so use the former logic.
[self _dispatchSyncModeChange:event];
} else {
// If the previous mode was prerender and the result of dispatching that event was committed, we do not need
// to dispatch an event for visible.
const auto wasPrerenderCommitted = oldMode.has_value() && oldMode == RCTVirtualViewModePrerender &&
_renderState == RCTVirtualViewRenderStateRendered;
if (!wasPrerenderCommitted) {
[self _dispatchSyncModeChange:event];
}
}
break;
case RCTVirtualViewModePrerender:
if (!oldMode.has_value() || oldMode != RCTVirtualViewModeVisible) {
[self _dispatchAsyncModeChange:event];
}
break;
case RCTVirtualViewModeHidden:
[self _dispatchAsyncModeChange:event];
break;
}
if (ReactNativeFeatureFlags::hideOffscreenVirtualViewsOnIOS()) {
switch (newMode) {
case RCTVirtualViewModeVisible:
self.hidden = NO;
break;
case RCTVirtualViewModePrerender:
self.hidden = !sIsAccessibilityUsed;
break;
case RCTVirtualViewModeHidden:
self.hidden = YES;
break;
}
}
}
#pragma mark - Private API
- (void)_unhideIfNeeded
{
if (!sIsAccessibilityUsed) {
// accessibility is detected for the first time. Make views visible.
sIsAccessibilityUsed = YES;
}
if (self.hidden) {
self.hidden = NO;
}
}
- (id<RCTVirtualViewContainerProtocol>)_getParentVirtualViewContainer
{
UIView *view = self.superview;
while (view != nil) {
if ([view respondsToSelector:@selector(virtualViewContainerState)]) {
return (id<RCTVirtualViewContainerProtocol>)view;
}
view = view.superview;
}
return nil;
}
- (void)_dispatchAsyncModeChange:(VirtualViewEventEmitter::OnModeChange &)event
{
if (!_eventEmitter) {
return;
}
std::shared_ptr<const VirtualViewEventEmitter> emitter =
std::static_pointer_cast<const VirtualViewEventEmitter>(_eventEmitter);
emitter->onModeChange(event);
}
- (void)_dispatchSyncModeChange:(VirtualViewEventEmitter::OnModeChange &)event
{
if (!_eventEmitter) {
return;
}
std::shared_ptr<const VirtualViewEventEmitter> emitter =
std::static_pointer_cast<const VirtualViewEventEmitter>(_eventEmitter);
// TODO: Move this into a custom event emitter. We had to duplicate the codegen code here from onModeChange in order
// to dispatch synchronously and discrete.
emitter->experimental_flushSync([&emitter, &event]() {
emitter->dispatchEvent(
"modeChange",
[event](jsi::Runtime &runtime) {
auto payload = jsi::Object(runtime);
payload.setProperty(runtime, "mode", event.mode);
{
auto targetRect = jsi::Object(runtime);
targetRect.setProperty(runtime, "x", event.targetRect.x);
targetRect.setProperty(runtime, "y", event.targetRect.y);
targetRect.setProperty(runtime, "width", event.targetRect.width);
targetRect.setProperty(runtime, "height", event.targetRect.height);
payload.setProperty(runtime, "targetRect", targetRect);
}
{
auto thresholdRect = jsi::Object(runtime);
thresholdRect.setProperty(runtime, "x", event.thresholdRect.x);
thresholdRect.setProperty(runtime, "y", event.thresholdRect.y);
thresholdRect.setProperty(runtime, "width", event.thresholdRect.width);
thresholdRect.setProperty(runtime, "height", event.thresholdRect.height);
payload.setProperty(runtime, "thresholdRect", thresholdRect);
}
return payload;
},
RawEvent::Category::Discrete);
});
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<VirtualViewExperimentalComponentDescriptor>();
}
@end
Class<RCTComponentViewProtocol> VirtualViewExperimentalCls(void)
{
return RCTVirtualViewExperimentalComponentView.class;
}

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 <Foundation/Foundation.h>
#ifndef RCTVirtualViewMode_h
#define RCTVirtualViewMode_h
// Enum for virtual view modes
using RCTVirtualViewMode = NS_ENUM(NSInteger){
RCTVirtualViewModeVisible = 0,
RCTVirtualViewModePrerender = 1,
RCTVirtualViewModeHidden = 2,
};
#endif /* RCTVirtualViewMode_h */

View File

@@ -0,0 +1,17 @@
/*
* 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>
#ifndef RCTVirtualViewRenderState_h
#define RCTVirtualViewRenderState_h
using RCTVirtualViewRenderState = NS_ENUM(NSInteger){
RCTVirtualViewRenderStateUnknown = 0,
RCTVirtualViewRenderStateRendered = 1,
RCTVirtualViewRenderStateNone = 2,
};
#endif /* RCTVirtualViewRenderState_h */