Files
react-native/packages/react-native/React/Fabric/Mounting/ComponentViews/View/RCTViewComponentView.mm
T
Saad Najmi b70709dbc2 fix(iOS): Replace uses of CGColorRef with UIColor to avoid manual memory management (#46847)
Summary:
Update React Native on iOS to do less manual memory management, by replacing uses of `CGColorRef` with `UIColor`, and only calling `UIColor.CGColor` when needed. This results in much less manual memory management, which is probably a good thing in 2024 :D. The downside is there is a breaking change: the signature of a method in `RCTBorderDrawing` changes.

This is a followup to https://github.com/facebook/react-native/issues/46797 . After that PR merged and I tested merging React Native macOS to the `0.76-stable` branch cut commit again, I saw even more places I needed to manually call `CGColorRetain` / `CGColorRelease`. The reason is due to React Native macOS specifics (explained below) and I could update React Native macOS to not need these changes, but I thought I would at least throw up a PR to propose the changes, as it may be good to move away from using Core Graphics' C base API as much as possible.

## Longer Explanation

With https://github.com/microsoft/react-native-macos/pull/2209 , I wrote a shim of [UIGraphicsImageRenderer](https://developer.apple.com/documentation/uikit/uigraphicsimagerenderer) for macOS. The main difference is my shim calls the NSImage API [imageWithSize:flipped:drawingHandler:](https://developer.apple.com/documentation/appkit/nsimage/1519860-imagewithsize?language=objc) to shim the UIGraphicsImageRenderer API [imageWithData:](https://developer.apple.com/documentation/uikit/uiimage/1624137-imagewithdata). The difference between the two is that the macOS API copies the block, and executes it when Appkit is about to draw the image, while the iOS API executes the block right away. Because of this, I think I am hitting way more places where `CGColorRef` variables needed to be retained / released. Additionally, I hit more of them when I merge to the 0.76 branch cut commit (this is why I didn't catch it with https://github.com/facebook/react-native/issues/46797).

Given this constraint, I have a couple of options:
1. Refactor my macOS shim to use the deprecated API [[NSImage lockFocus]](https://developer.apple.com/documentation/appkit/nsimage/1519891-lockfocus)
    - I am not a fan of this because `lockFocus` was deprecated for `imageWithSize:flipped:drawingHandler:`
2. Refactor my macOS shim to do what we used to do: Create a CGContext manually and write it to an image
    - This is probably OK. Relies on less Appkit specifics, and potentially gives more control to RN on the rendering layer, which I recall lenaic told me is better for Fabric)
3. Refactor React Native to avoid CGColorRef altogether, and use `UIColor` (which ARC will memory manage for us) as much as possible.
    - This is the approach of this PR and my preferred approach. The downside is this changes the signature of some of the methods in `RCTBorderDrawing` which is a breaking change. I've seen other PRs do this, so maybe its OK?

## Changelog:

[IOS] [BREAKING] -  Replace uses of `CGColorRef` with UIColor to avoid manual memory management

Pull Request resolved: https://github.com/facebook/react-native/pull/46847

Test Plan: Launching RNTester's View example (which tests a lot of the border / outline / shadow rendering) does not crash for me on both iOS and macOS.

Reviewed By: NickGerleman

Differential Revision: D63989547

Pulled By: joevilches

fbshipit-source-id: 5e85e17567e3dd8b4b0388452398954ad2e02464
2024-10-09 16:42:17 -07:00

1350 lines
48 KiB
Plaintext

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#import "RCTViewComponentView.h"
#import <CoreGraphics/CoreGraphics.h>
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
#import <React/RCTAssert.h>
#import <React/RCTBorderDrawing.h>
#import <React/RCTBoxShadow.h>
#import <React/RCTConversions.h>
#import <React/RCTLocalizedString.h>
#import <react/featureflags/ReactNativeFeatureFlags.h>
#import <react/renderer/components/view/ViewComponentDescriptor.h>
#import <react/renderer/components/view/ViewEventEmitter.h>
#import <react/renderer/components/view/ViewProps.h>
#import <react/renderer/components/view/accessibilityPropsConversions.h>
#import <react/renderer/graphics/BlendMode.h>
#ifdef RCT_DYNAMIC_FRAMEWORKS
#import <React/RCTComponentViewFactory.h>
#endif
using namespace facebook::react;
const CGFloat BACKGROUND_COLOR_ZPOSITION = -1024.0f;
@implementation RCTViewComponentView {
UIColor *_backgroundColor;
CALayer *_backgroundColorLayer;
__weak CALayer *_borderLayer;
CALayer *_outlineLayer;
CALayer *_boxShadowLayer;
CALayer *_filterLayer;
NSMutableArray<CAGradientLayer *> *_gradientLayers;
BOOL _needsInvalidateLayer;
BOOL _isJSResponder;
BOOL _removeClippedSubviews;
NSMutableArray<UIView *> *_reactSubviews;
NSSet<NSString *> *_Nullable _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
UIView *_containerView;
BOOL _useCustomContainerView;
}
#ifdef RCT_DYNAMIC_FRAMEWORKS
+ (void)load
{
[RCTComponentViewFactory.currentComponentViewFactory registerComponentViewClass:self];
}
#endif
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
_props = ViewShadowNode::defaultSharedProps();
_reactSubviews = [NSMutableArray new];
self.multipleTouchEnabled = YES;
_useCustomContainerView = NO;
}
return self;
}
- (facebook::react::Props::Shared)props
{
return _props;
}
- (void)setContentView:(UIView *)contentView
{
if (_contentView) {
[_contentView removeFromSuperview];
}
_contentView = contentView;
if (_contentView) {
[self.currentContainerView addSubview:_contentView];
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
}
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
return [super pointInside:point withEvent:event];
}
CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}
- (UIColor *)backgroundColor
{
return _backgroundColor;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
_backgroundColor = backgroundColor;
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
[super traitCollectionDidChange:previousTraitCollection];
if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
[self invalidateLayer];
}
}
#pragma mark - RCTComponentViewProtocol
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
RCTAssert(
self == [RCTViewComponentView class],
@"`+[RCTComponentViewProtocol componentDescriptorProvider]` must be implemented for all subclasses (and `%@` particularly).",
NSStringFromClass([self class]));
return concreteComponentDescriptorProvider<ViewComponentDescriptor>();
}
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
childComponentView.superview == nil,
@"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)",
self,
childComponentView,
@(index),
@([childComponentView.superview tag]));
if (_removeClippedSubviews) {
[_reactSubviews insertObject:childComponentView atIndex:index];
} else {
[self.currentContainerView insertSubview:childComponentView atIndex:index];
}
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
if (_removeClippedSubviews) {
[_reactSubviews removeObjectAtIndex:index];
} else {
RCTAssert(
childComponentView.superview == self.currentContainerView,
@"Attempt to unmount a view which is mounted inside different view. (parent: %@, child: %@, index: %@)",
self,
childComponentView,
@(index));
RCTAssert(
(self.currentContainerView.subviews.count > index) &&
[self.currentContainerView.subviews objectAtIndex:index] == childComponentView,
@"Attempt to unmount a view which has a different index. (parent: %@, child: %@, index: %@, actual index: %@, tag at index: %@)",
self,
childComponentView,
@(index),
@([self.currentContainerView.subviews indexOfObject:childComponentView]),
@([[self.currentContainerView.subviews objectAtIndex:index] tag]));
}
[childComponentView removeFromSuperview];
}
- (void)updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
{
if (!_removeClippedSubviews) {
// Use default behavior if unmounting is disabled
return [super updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
}
if (_reactSubviews.count == 0) {
// Do nothing if we have no subviews
return;
}
if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
// Do nothing if layout hasn't happened yet
return;
}
// Convert clipping rect to local coordinates
clipRect = [clipView convertRect:clipRect toView:self];
// Mount / unmount views
for (UIView *view in _reactSubviews) {
if (CGRectIntersectsRect(clipRect, view.frame)) {
// View is at least partially visible, so remount it if unmounted
[self.currentContainerView addSubview:view];
// View is visible, update clipped subviews
[view updateClippedSubviewsWithClipRect:clipRect relativeToView:self];
} else if (view.superview) {
// View is completely outside the clipRect, so unmount it
[view removeFromSuperview];
}
}
}
- (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &)oldProps
{
RCTAssert(props, @"`props` must not be `null`.");
#ifndef NS_BLOCK_ASSERTIONS
auto propsRawPtr = _props.get();
RCTAssert(
propsRawPtr &&
([self class] == [RCTViewComponentView class] ||
typeid(*propsRawPtr).hash_code() != typeid(const ViewProps).hash_code()),
@"`RCTViewComponentView` subclasses (and `%@` particularly) must setup `_props`"
" instance variable with a default value in the constructor.",
NSStringFromClass([self class]));
#endif
const auto &oldViewProps = static_cast<const ViewProps &>(*_props);
const auto &newViewProps = static_cast<const ViewProps &>(*props);
BOOL needsInvalidateLayer = NO;
// `opacity`
if (oldViewProps.opacity != newViewProps.opacity &&
![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"opacity"]) {
self.layer.opacity = (float)newViewProps.opacity;
needsInvalidateLayer = YES;
}
if (oldViewProps.removeClippedSubviews != newViewProps.removeClippedSubviews) {
_removeClippedSubviews = newViewProps.removeClippedSubviews;
if (_removeClippedSubviews && self.currentContainerView.subviews.count > 0) {
_reactSubviews = [NSMutableArray arrayWithArray:self.currentContainerView.subviews];
}
}
// `backgroundColor`
if (oldViewProps.backgroundColor != newViewProps.backgroundColor) {
self.backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor);
needsInvalidateLayer = YES;
}
// `shadowColor`
if (oldViewProps.shadowColor != newViewProps.shadowColor) {
UIColor *shadowColor = RCTUIColorFromSharedColor(newViewProps.shadowColor);
self.layer.shadowColor = shadowColor.CGColor;
needsInvalidateLayer = YES;
}
// `shadowOffset`
if (oldViewProps.shadowOffset != newViewProps.shadowOffset) {
self.layer.shadowOffset = RCTCGSizeFromSize(newViewProps.shadowOffset);
needsInvalidateLayer = YES;
}
// `shadowOpacity`
if (oldViewProps.shadowOpacity != newViewProps.shadowOpacity) {
self.layer.shadowOpacity = (float)newViewProps.shadowOpacity;
needsInvalidateLayer = YES;
}
// `shadowRadius`
if (oldViewProps.shadowRadius != newViewProps.shadowRadius) {
self.layer.shadowRadius = (CGFloat)newViewProps.shadowRadius;
needsInvalidateLayer = YES;
}
// `backfaceVisibility`
if (oldViewProps.backfaceVisibility != newViewProps.backfaceVisibility) {
self.layer.doubleSided = newViewProps.backfaceVisibility == BackfaceVisibility::Visible;
}
// `cursor`
if (oldViewProps.cursor != newViewProps.cursor) {
needsInvalidateLayer = YES;
}
// `shouldRasterize`
if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) {
self.layer.shouldRasterize = newViewProps.shouldRasterize;
self.layer.rasterizationScale = newViewProps.shouldRasterize ? self.traitCollection.displayScale : 1.0;
}
// `pointerEvents`
if (oldViewProps.pointerEvents != newViewProps.pointerEvents) {
self.userInteractionEnabled = newViewProps.pointerEvents != PointerEventsMode::None;
}
// `transform`
if ((oldViewProps.transform != newViewProps.transform ||
oldViewProps.transformOrigin != newViewProps.transformOrigin) &&
![_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
auto newTransform = newViewProps.resolveTransform(_layoutMetrics);
CATransform3D caTransform = RCTCATransform3DFromTransformMatrix(newTransform);
self.layer.transform = caTransform;
// Enable edge antialiasing in rotation, skew, or perspective transforms
self.layer.allowsEdgeAntialiasing = caTransform.m12 != 0.0f || caTransform.m21 != 0.0f || caTransform.m34 != 0.0f;
}
// `hitSlop`
if (oldViewProps.hitSlop != newViewProps.hitSlop) {
self.hitTestEdgeInsets = {
-newViewProps.hitSlop.top,
-newViewProps.hitSlop.left,
-newViewProps.hitSlop.bottom,
-newViewProps.hitSlop.right};
}
// `overflow`
if (oldViewProps.getClipsContentToBounds() != newViewProps.getClipsContentToBounds()) {
self.currentContainerView.clipsToBounds = newViewProps.getClipsContentToBounds();
needsInvalidateLayer = YES;
}
// `border`
if (oldViewProps.borderStyles != newViewProps.borderStyles || oldViewProps.borderRadii != newViewProps.borderRadii ||
oldViewProps.borderColors != newViewProps.borderColors) {
needsInvalidateLayer = YES;
}
// `outline`
if (oldViewProps.outlineStyle != newViewProps.outlineStyle ||
oldViewProps.outlineColor != newViewProps.outlineColor ||
oldViewProps.outlineOffset != newViewProps.outlineOffset ||
oldViewProps.outlineWidth != newViewProps.outlineWidth) {
needsInvalidateLayer = YES;
}
// `nativeId`
if (oldViewProps.nativeId != newViewProps.nativeId) {
self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId);
}
// `accessible`
if (oldViewProps.accessible != newViewProps.accessible) {
self.accessibilityElement.isAccessibilityElement = newViewProps.accessible;
}
// `accessibilityLabel`
if (oldViewProps.accessibilityLabel != newViewProps.accessibilityLabel) {
self.accessibilityElement.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLabel);
}
// `accessibilityLanguage`
if (oldViewProps.accessibilityLanguage != newViewProps.accessibilityLanguage) {
self.accessibilityElement.accessibilityLanguage =
RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLanguage);
}
// `accessibilityHint`
if (oldViewProps.accessibilityHint != newViewProps.accessibilityHint) {
self.accessibilityElement.accessibilityHint = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint);
}
// `accessibilityViewIsModal`
if (oldViewProps.accessibilityViewIsModal != newViewProps.accessibilityViewIsModal) {
self.accessibilityElement.accessibilityViewIsModal = newViewProps.accessibilityViewIsModal;
}
// `accessibilityElementsHidden`
if (oldViewProps.accessibilityElementsHidden != newViewProps.accessibilityElementsHidden) {
self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden;
}
// `accessibilityShowsLargeContentViewer`
if (oldViewProps.accessibilityShowsLargeContentViewer != newViewProps.accessibilityShowsLargeContentViewer) {
if (@available(iOS 13.0, *)) {
if (newViewProps.accessibilityShowsLargeContentViewer) {
self.showsLargeContentViewer = YES;
UILargeContentViewerInteraction *interaction = [[UILargeContentViewerInteraction alloc] init];
[self addInteraction:interaction];
} else {
self.showsLargeContentViewer = NO;
}
}
}
// `accessibilityLargeContentTitle`
if (oldViewProps.accessibilityLargeContentTitle != newViewProps.accessibilityLargeContentTitle) {
if (@available(iOS 13.0, *)) {
self.largeContentTitle = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLargeContentTitle);
}
}
// `accessibilityTraits`
if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) {
self.accessibilityElement.accessibilityTraits =
RCTUIAccessibilityTraitsFromAccessibilityTraits(newViewProps.accessibilityTraits);
}
// `accessibilityState`
if (oldViewProps.accessibilityState != newViewProps.accessibilityState) {
self.accessibilityTraits &= ~(UIAccessibilityTraitNotEnabled | UIAccessibilityTraitSelected);
const auto accessibilityState = newViewProps.accessibilityState.value_or(AccessibilityState{});
if (accessibilityState.selected) {
self.accessibilityTraits |= UIAccessibilityTraitSelected;
}
if (accessibilityState.disabled) {
self.accessibilityTraits |= UIAccessibilityTraitNotEnabled;
}
}
// `accessibilityIgnoresInvertColors`
if (oldViewProps.accessibilityIgnoresInvertColors != newViewProps.accessibilityIgnoresInvertColors) {
self.accessibilityIgnoresInvertColors = newViewProps.accessibilityIgnoresInvertColors;
}
// `accessibilityValue`
if (oldViewProps.accessibilityValue != newViewProps.accessibilityValue) {
if (newViewProps.accessibilityValue.text.has_value()) {
self.accessibilityElement.accessibilityValue =
RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityValue.text.value());
} else if (
newViewProps.accessibilityValue.now.has_value() && newViewProps.accessibilityValue.min.has_value() &&
newViewProps.accessibilityValue.max.has_value()) {
CGFloat val = (CGFloat)(newViewProps.accessibilityValue.now.value()) /
(newViewProps.accessibilityValue.max.value() - newViewProps.accessibilityValue.min.value());
self.accessibilityElement.accessibilityValue =
[NSNumberFormatter localizedStringFromNumber:@(val) numberStyle:NSNumberFormatterPercentStyle];
;
} else {
self.accessibilityElement.accessibilityValue = nil;
}
}
// `testId`
if (oldViewProps.testId != newViewProps.testId) {
SEL setAccessibilityIdentifierSelector = @selector(setAccessibilityIdentifier:);
NSString *identifier = RCTNSStringFromString(newViewProps.testId);
if ([self.accessibilityElement respondsToSelector:setAccessibilityIdentifierSelector]) {
UIView *accessibilityView = (UIView *)self.accessibilityElement;
accessibilityView.accessibilityIdentifier = identifier;
} else {
self.accessibilityIdentifier = identifier;
}
}
// `filter`
if (oldViewProps.filter != newViewProps.filter) {
needsInvalidateLayer = YES;
}
// `mixBlendMode`
if (oldViewProps.mixBlendMode != newViewProps.mixBlendMode) {
switch (newViewProps.mixBlendMode) {
case BlendMode::Multiply:
self.layer.compositingFilter = @"multiplyBlendMode";
break;
case BlendMode::Screen:
self.layer.compositingFilter = @"screenBlendMode";
break;
case BlendMode::Overlay:
self.layer.compositingFilter = @"overlayBlendMode";
break;
case BlendMode::Darken:
self.layer.compositingFilter = @"darkenBlendMode";
break;
case BlendMode::Lighten:
self.layer.compositingFilter = @"lightenBlendMode";
break;
case BlendMode::ColorDodge:
self.layer.compositingFilter = @"colorDodgeBlendMode";
break;
case BlendMode::ColorBurn:
self.layer.compositingFilter = @"colorBurnBlendMode";
break;
case BlendMode::HardLight:
self.layer.compositingFilter = @"hardLightBlendMode";
break;
case BlendMode::SoftLight:
self.layer.compositingFilter = @"softLightBlendMode";
break;
case BlendMode::Difference:
self.layer.compositingFilter = @"differenceBlendMode";
break;
case BlendMode::Exclusion:
self.layer.compositingFilter = @"exclusionBlendMode";
break;
case BlendMode::Hue:
self.layer.compositingFilter = @"hueBlendMode";
break;
case BlendMode::Saturation:
self.layer.compositingFilter = @"saturationBlendMode";
break;
case BlendMode::Color:
self.layer.compositingFilter = @"colorBlendMode";
break;
case BlendMode::Luminosity:
self.layer.compositingFilter = @"luminosityBlendMode";
break;
case BlendMode::Normal:
self.layer.compositingFilter = nil;
break;
}
}
// `linearGradient`
if (oldViewProps.backgroundImage != newViewProps.backgroundImage) {
needsInvalidateLayer = YES;
}
// `boxShadow`
if (oldViewProps.boxShadow != newViewProps.boxShadow) {
needsInvalidateLayer = YES;
}
_needsInvalidateLayer = _needsInvalidateLayer || needsInvalidateLayer;
_props = std::static_pointer_cast<const ViewProps>(props);
}
- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
{
assert(std::dynamic_pointer_cast<const ViewEventEmitter>(eventEmitter));
_eventEmitter = std::static_pointer_cast<const ViewEventEmitter>(eventEmitter);
}
- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics
{
// Using stored `_layoutMetrics` as `oldLayoutMetrics` here to avoid
// re-applying individual sub-values which weren't changed.
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:_layoutMetrics];
_layoutMetrics = layoutMetrics;
_needsInvalidateLayer = YES;
_borderLayer.frame = self.layer.bounds;
if (_contentView) {
_contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame());
}
if (_containerView) {
_containerView.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height);
}
if (_backgroundColorLayer) {
_backgroundColorLayer.frame = CGRectMake(0, 0, self.layer.bounds.size.width, self.layer.bounds.size.height);
}
if ((_props->transformOrigin.isSet() || _props->transform.operations.size() > 0) &&
layoutMetrics.frame.size != oldLayoutMetrics.frame.size) {
auto newTransform = _props->resolveTransform(layoutMetrics);
self.layer.transform = RCTCATransform3DFromTransformMatrix(newTransform);
}
}
- (BOOL)isJSResponder
{
return _isJSResponder;
}
- (void)setIsJSResponder:(BOOL)isJSResponder
{
_isJSResponder = isJSResponder;
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[super finalizeUpdates:updateMask];
_useCustomContainerView = [self styleWouldClipOverflowInk];
if (!_needsInvalidateLayer) {
return;
}
_needsInvalidateLayer = NO;
[self invalidateLayer];
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
// If view was managed by animated, its props need to align with UIView's properties.
const auto &props = static_cast<const ViewProps &>(*_props);
if ([_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"transform"]) {
self.layer.transform = RCTCATransform3DFromTransformMatrix(props.transform);
}
if ([_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN containsObject:@"opacity"]) {
self.layer.opacity = (float)props.opacity;
}
_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = nil;
_eventEmitter.reset();
_isJSResponder = NO;
_removeClippedSubviews = NO;
_reactSubviews = [NSMutableArray new];
}
- (void)setPropKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN:(NSSet<NSString *> *_Nullable)props
{
_propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN = props;
}
- (NSSet<NSString *> *_Nullable)propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN
{
return _propKeysManagedByAnimated_DO_NOT_USE_THIS_IS_BROKEN;
}
- (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// This is a classic textbook implementation of `hitTest:` with a couple of improvements:
// * It does not stop algorithm if some touch is outside the view
// which does not have `clipToBounds` enabled.
// * Taking `layer.zIndex` field into an account is not required because
// lists of `ShadowView`s are already sorted based on `zIndex` prop.
if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) {
return nil;
}
BOOL isPointInside = [self pointInside:point withEvent:event];
BOOL clipsToBounds = self.currentContainerView.clipsToBounds;
clipsToBounds = clipsToBounds || _layoutMetrics.overflowInset == EdgeInsets{};
if (clipsToBounds && !isPointInside) {
return nil;
}
for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event];
if (hitView) {
return hitView;
}
}
return isPointInside ? self : nil;
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
switch (_props->pointerEvents) {
case PointerEventsMode::Auto:
return [self betterHitTest:point withEvent:event];
case PointerEventsMode::None:
return nil;
case PointerEventsMode::BoxOnly:
return [self pointInside:point withEvent:event] ? self : nil;
case PointerEventsMode::BoxNone:
UIView *view = [self betterHitTest:point withEvent:event];
return view != self ? view : nil;
}
}
static RCTCornerRadii RCTCornerRadiiFromBorderRadii(BorderRadii borderRadii)
{
return RCTCornerRadii{
.topLeftHorizontal = (CGFloat)borderRadii.topLeft.horizontal,
.topLeftVertical = (CGFloat)borderRadii.topLeft.vertical,
.topRightHorizontal = (CGFloat)borderRadii.topRight.horizontal,
.topRightVertical = (CGFloat)borderRadii.topRight.vertical,
.bottomLeftHorizontal = (CGFloat)borderRadii.bottomLeft.horizontal,
.bottomLeftVertical = (CGFloat)borderRadii.bottomLeft.vertical,
.bottomRightHorizontal = (CGFloat)borderRadii.bottomRight.horizontal,
.bottomRightVertical = (CGFloat)borderRadii.bottomRight.vertical};
}
static RCTCornerRadii RCTCreateOutlineCornerRadiiFromBorderRadii(const BorderRadii &borderRadii, CGFloat outlineWidth)
{
return RCTCornerRadii{
borderRadii.topLeft.horizontal != 0 ? borderRadii.topLeft.horizontal + outlineWidth : 0,
borderRadii.topLeft.vertical != 0 ? borderRadii.topLeft.vertical + outlineWidth : 0,
borderRadii.topRight.horizontal != 0 ? borderRadii.topRight.horizontal + outlineWidth : 0,
borderRadii.topRight.vertical != 0 ? borderRadii.topRight.vertical + outlineWidth : 0,
borderRadii.bottomLeft.horizontal != 0 ? borderRadii.bottomLeft.horizontal + outlineWidth : 0,
borderRadii.bottomLeft.vertical != 0 ? borderRadii.bottomLeft.vertical + outlineWidth : 0,
borderRadii.bottomRight.horizontal != 0 ? borderRadii.bottomRight.horizontal + outlineWidth : 0,
borderRadii.bottomRight.vertical != 0 ? borderRadii.bottomRight.vertical + outlineWidth : 0};
}
// To be used for CSS properties like `border` and `outline`.
static void RCTAddContourEffectToLayer(
CALayer *layer,
const RCTCornerRadii &cornerRadii,
const RCTBorderColors &contourColors,
const UIEdgeInsets &contourInsets,
const RCTBorderStyle &contourStyle)
{
UIImage *image = RCTGetBorderImage(
contourStyle, layer.bounds.size, cornerRadii, contourInsets, contourColors, [UIColor clearColor], NO);
if (image == nil) {
layer.contents = nil;
} else {
CGSize imageSize = image.size;
UIEdgeInsets imageCapInsets = image.capInsets;
CGRect contentsCenter = CGRect{
CGPoint{imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height},
CGSize{(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height}};
layer.contents = (id)image.CGImage;
layer.contentsScale = image.scale;
BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
if (isResizable) {
layer.contentsCenter = contentsCenter;
} else {
layer.contentsCenter = CGRect{CGPoint{0.0, 0.0}, CGSize{1.0, 1.0}};
}
}
// If mutations are applied inside of Animation block, it may cause layer to be animated.
// To stop that, imperatively remove all animations from layer.
[layer removeAllAnimations];
}
static RCTBorderColors RCTCreateRCTBorderColorsFromBorderColors(BorderColors borderColors)
{
return RCTBorderColors{
.top = RCTUIColorFromSharedColor(borderColors.top),
.left = RCTUIColorFromSharedColor(borderColors.left),
.bottom = RCTUIColorFromSharedColor(borderColors.bottom),
.right = RCTUIColorFromSharedColor(borderColors.right)};
}
static CALayerCornerCurve CornerCurveFromBorderCurve(BorderCurve borderCurve)
{
// The constants are available only starting from iOS 13
// CALayerCornerCurve is a typealias on NSString *
switch (borderCurve) {
case BorderCurve::Continuous:
return @"continuous"; // kCACornerCurveContinuous;
case BorderCurve::Circular:
return @"circular"; // kCACornerCurveCircular;
}
}
static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle)
{
switch (borderStyle) {
case BorderStyle::Solid:
return RCTBorderStyleSolid;
case BorderStyle::Dotted:
return RCTBorderStyleDotted;
case BorderStyle::Dashed:
return RCTBorderStyleDashed;
}
}
static RCTBorderStyle RCTBorderStyleFromOutlineStyle(OutlineStyle outlineStyle)
{
switch (outlineStyle) {
case OutlineStyle::Solid:
return RCTBorderStyleSolid;
case OutlineStyle::Dotted:
return RCTBorderStyleDotted;
case OutlineStyle::Dashed:
return RCTBorderStyleDashed;
}
}
- (BOOL)styleWouldClipOverflowInk
{
const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
BOOL nonZeroBorderWidth = !(borderMetrics.borderWidths.isUniform() && borderMetrics.borderWidths.left == 0);
BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
return _props->getClipsContentToBounds() &&
((!_props->boxShadow.empty() || (clipToPaddingBox && nonZeroBorderWidth)) || _props->outlineWidth != 0);
}
// This UIView is the UIView that holds all subviews. It is sometimes not self
// because we want to render "overflow ink" that extends beyond the bounds of
// the view and is not affected by clipping.
- (UIView *)currentContainerView
{
if (_useCustomContainerView) {
if (!_containerView) {
_containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
for (UIView *subview in self.subviews) {
[_containerView addSubview:subview];
}
_containerView.clipsToBounds = self.clipsToBounds;
self.clipsToBounds = NO;
_containerView.layer.mask = self.layer.mask;
self.layer.mask = nil;
[self addSubview:_containerView];
}
return _containerView;
} else {
if (_containerView) {
for (UIView *subview in _containerView.subviews) {
[self addSubview:subview];
}
self.clipsToBounds = _containerView.clipsToBounds;
self.layer.mask = _containerView.layer.mask;
[_containerView removeFromSuperview];
_containerView = nil;
}
return self;
}
}
- (void)invalidateLayer
{
CALayer *layer = self.layer;
if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) {
return;
}
const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics);
// Stage 1. Shadow Path
BOOL const layerHasShadow = layer.shadowOpacity > 0 && CGColorGetAlpha(layer.shadowColor) > 0;
if (layerHasShadow) {
if (CGColorGetAlpha(_backgroundColor.CGColor) > 0.999) {
// If view has a solid background color, calculate shadow path from border.
const RCTCornerInsets cornerInsets =
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil);
layer.shadowPath = shadowPath;
CGPathRelease(shadowPath);
} else {
// Can't accurately calculate box shadow, so fall back to pixel-based shadow.
layer.shadowPath = nil;
}
} else {
layer.shadowPath = nil;
}
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 170000 /* __IPHONE_17_0 */
// Stage 1.5. Cursor / Hover Effects
if (@available(iOS 17.0, *)) {
UIHoverStyle *hoverStyle = nil;
if (_props->cursor == Cursor::Pointer) {
const RCTCornerInsets cornerInsets =
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
#if TARGET_OS_IOS
// Due to an Apple bug, it seems on iOS, UIShapes made with `[UIShape shapeWithBezierPath:]`
// evaluate their shape on the superviews' coordinate space. This leads to the hover shape
// rendering incorrectly on iOS, iOS apps in compatibility mode on visionOS, but not on visionOS.
// To work around this, for iOS, we can calculate the border path based on `view.frame` (the
// superview's coordinate space) instead of view.bounds.
CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.frame, cornerInsets, NULL);
#else // TARGET_OS_VISION
CGPathRef borderPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, NULL);
#endif
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:borderPath];
CGPathRelease(borderPath);
UIShape *shape = [UIShape shapeWithBezierPath:bezierPath];
hoverStyle = [UIHoverStyle styleWithEffect:[UIHoverAutomaticEffect effect] shape:shape];
}
[self setHoverStyle:hoverStyle];
}
#endif
const bool useCoreAnimationBorderRendering =
borderMetrics.borderColors.isUniform() && borderMetrics.borderWidths.isUniform() &&
borderMetrics.borderStyles.isUniform() && borderMetrics.borderRadii.isUniform() &&
(
// iOS draws borders in front of the content whereas CSS draws them behind
// the content. For this reason, only use iOS border drawing when clipping
// or when the border is hidden.
borderMetrics.borderWidths.left == 0 || self.currentContainerView.clipsToBounds ||
(colorComponentsFromColor(borderMetrics.borderColors.left).alpha == 0 &&
(*borderMetrics.borderColors.left).getUIColor() != nullptr));
// background color
UIColor *backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection];
// The reason we sometimes do not set self.layer's backgroundColor is because
// we want to support non-uniform border radii, which apple does not natively
// support. To get this behavior we need to create a CGPath in the shape that
// we want. If we mask self.layer to this path, we would be clipping subviews
// which we may not want to do. The generalized solution in this case is just
// create a new layer
if (useCoreAnimationBorderRendering) {
[_backgroundColorLayer removeFromSuperlayer];
_backgroundColorLayer = nil;
layer.backgroundColor = backgroundColor.CGColor;
} else {
layer.backgroundColor = nil;
if (!_backgroundColorLayer) {
_backgroundColorLayer = [CALayer layer];
_backgroundColorLayer.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
_backgroundColorLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
[self.layer addSublayer:_backgroundColorLayer];
}
_backgroundColorLayer.backgroundColor = backgroundColor.CGColor;
if (borderMetrics.borderRadii.isUniform()) {
_backgroundColorLayer.mask = nil;
_backgroundColorLayer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
_backgroundColorLayer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
} else {
CAShapeLayer *maskLayer =
[self createMaskLayer:self.bounds
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
_backgroundColorLayer.mask = maskLayer;
_backgroundColorLayer.cornerRadius = 0;
}
}
// borders
if (useCoreAnimationBorderRendering) {
[_borderLayer removeFromSuperlayer];
_borderLayer = nil;
layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left;
UIColor *borderColor = RCTUIColorFromSharedColor(borderMetrics.borderColors.left);
layer.borderColor = borderColor.CGColor;
layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft.horizontal;
layer.cornerCurve = CornerCurveFromBorderCurve(borderMetrics.borderCurves.topLeft);
} else {
if (!_borderLayer) {
CALayer *borderLayer = [CALayer new];
borderLayer.zPosition = BACKGROUND_COLOR_ZPOSITION + 1;
borderLayer.frame = layer.bounds;
borderLayer.magnificationFilter = kCAFilterNearest;
[layer addSublayer:borderLayer];
_borderLayer = borderLayer;
}
layer.borderWidth = 0;
layer.borderColor = nil;
layer.cornerRadius = 0;
RCTBorderColors borderColors = RCTCreateRCTBorderColorsFromBorderColors(borderMetrics.borderColors);
RCTAddContourEffectToLayer(
_borderLayer,
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
borderColors,
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left));
}
// outline
[_outlineLayer removeFromSuperlayer];
_outlineLayer = nil;
if (_props->outlineWidth != 0) {
if (!_outlineLayer) {
CALayer *outlineLayer = [CALayer new];
outlineLayer.magnificationFilter = kCAFilterNearest;
outlineLayer.zPosition = BACKGROUND_COLOR_ZPOSITION + 2;
[layer addSublayer:outlineLayer];
_outlineLayer = outlineLayer;
}
_outlineLayer.frame = CGRectInset(
layer.bounds, -_props->outlineOffset - _props->outlineWidth, -_props->outlineOffset - _props->outlineWidth);
if (borderMetrics.borderRadii.isUniform() && borderMetrics.borderRadii.topLeft.horizontal == 0) {
UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor);
_outlineLayer.borderWidth = _props->outlineWidth;
_outlineLayer.borderColor = outlineColor.CGColor;
} else {
UIColor *outlineColor = RCTUIColorFromSharedColor(_props->outlineColor);
RCTAddContourEffectToLayer(
_outlineLayer,
RCTCreateOutlineCornerRadiiFromBorderRadii(borderMetrics.borderRadii, _props->outlineWidth),
RCTBorderColors{outlineColor, outlineColor, outlineColor, outlineColor},
UIEdgeInsets{_props->outlineWidth, _props->outlineWidth, _props->outlineWidth, _props->outlineWidth},
RCTBorderStyleFromOutlineStyle(_props->outlineStyle));
}
}
// filter
[_filterLayer removeFromSuperlayer];
_filterLayer = nil;
self.layer.opacity = (float)_props->opacity;
if (!_props->filter.empty()) {
float multiplicativeBrightness = 1;
for (const auto &primitive : _props->filter) {
if (std::holds_alternative<Float>(primitive.parameters)) {
if (primitive.type == FilterType::Brightness) {
multiplicativeBrightness *= std::get<Float>(primitive.parameters);
} else if (primitive.type == FilterType::Opacity) {
self.layer.opacity *= std::get<Float>(primitive.parameters);
}
}
}
_filterLayer = [CALayer layer];
_filterLayer.frame = CGRectMake(0, 0, layer.frame.size.width, layer.frame.size.height);
_filterLayer.compositingFilter = @"multiplyBlendMode";
_filterLayer.backgroundColor = [UIColor colorWithRed:multiplicativeBrightness
green:multiplicativeBrightness
blue:multiplicativeBrightness
alpha:self.layer.opacity]
.CGColor;
if (borderMetrics.borderRadii.isUniform()) {
_filterLayer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
} else {
RCTCornerInsets cornerInsets =
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero);
_filterLayer.mask = [self createMaskLayer:self.bounds cornerInsets:cornerInsets];
}
// So that this layer is always above any potential sublayers this view may
// add
_filterLayer.zPosition = CGFLOAT_MAX;
[self.layer addSublayer:_filterLayer];
}
// background image
[self clearExistingGradientLayers];
if (!_props->backgroundImage.empty()) {
for (const auto &gradient : _props->backgroundImage) {
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
NSMutableArray *colors = [NSMutableArray array];
NSMutableArray *locations = [NSMutableArray array];
for (const auto &colorStop : gradient.colorStops) {
if (colorStop.position.has_value()) {
auto location = @(colorStop.position.value());
UIColor *color = RCTUIColorFromSharedColor(colorStop.color);
[colors addObject:(id)color.CGColor];
[locations addObject:location];
}
}
gradientLayer.startPoint = CGPointMake(gradient.startX, gradient.startY);
gradientLayer.endPoint = CGPointMake(gradient.endX, gradient.endY);
if (locations.count > 0) {
gradientLayer.locations = locations;
}
gradientLayer.colors = colors;
gradientLayer.frame = layer.bounds;
// border styling to work with gradient layers
if (useCoreAnimationBorderRendering) {
gradientLayer.borderWidth = layer.borderWidth;
gradientLayer.borderColor = layer.borderColor;
gradientLayer.cornerRadius = layer.cornerRadius;
gradientLayer.cornerCurve = layer.cornerCurve;
} else {
CAShapeLayer *maskLayer = [CAShapeLayer layer];
CGPathRef path = RCTPathCreateWithRoundedRect(
self.bounds,
RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero),
nil);
maskLayer.path = path;
CGPathRelease(path);
gradientLayer.mask = maskLayer;
}
gradientLayer.zPosition = BACKGROUND_COLOR_ZPOSITION;
[self.layer addSublayer:gradientLayer];
[_gradientLayers addObject:gradientLayer];
}
}
// box shadow
[_boxShadowLayer removeFromSuperlayer];
_boxShadowLayer = nil;
if (!_props->boxShadow.empty()) {
_boxShadowLayer = [CALayer layer];
[self.layer addSublayer:_boxShadowLayer];
_boxShadowLayer.zPosition = _borderLayer.zPosition;
_boxShadowLayer.frame = RCTGetBoundingRect(_props->boxShadow, self.layer.frame.size);
UIImage *boxShadowImage = RCTGetBoxShadowImage(
_props->boxShadow,
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths),
layer);
_boxShadowLayer.contents = (id)boxShadowImage.CGImage;
}
// clipping
self.currentContainerView.layer.mask = nil;
if (self.currentContainerView.clipsToBounds) {
BOOL clipToPaddingBox = ReactNativeFeatureFlags::enableIOSViewClipToPaddingBox();
if (!clipToPaddingBox) {
if (borderMetrics.borderRadii.isUniform()) {
self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
} else {
CALayer *maskLayer =
[self createMaskLayer:self.bounds
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero)];
self.currentContainerView.layer.mask = maskLayer;
}
for (UIView *subview in self.currentContainerView.subviews) {
if ([subview isKindOfClass:[UIImageView class]]) {
RCTCornerInsets cornerInsets = RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths));
// If the subview is an image view, we have to apply the mask directly to the image view's layer,
// otherwise the image might overflow with the border radius.
subview.layer.mask = [self createMaskLayer:subview.bounds cornerInsets:cornerInsets];
}
}
} else if (
!borderMetrics.borderWidths.isUniform() || borderMetrics.borderWidths.left != 0 ||
!borderMetrics.borderRadii.isUniform()) {
CALayer *maskLayer = [self createMaskLayer:RCTCGRectFromRect(_layoutMetrics.getPaddingFrame())
cornerInsets:RCTGetCornerInsets(
RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii),
RCTUIEdgeInsetsFromEdgeInsets(borderMetrics.borderWidths))];
self.currentContainerView.layer.mask = maskLayer;
} else {
self.currentContainerView.layer.cornerRadius = borderMetrics.borderRadii.topLeft.horizontal;
}
}
}
- (CAShapeLayer *)createMaskLayer:(CGRect)bounds cornerInsets:(RCTCornerInsets)cornerInsets
{
CGPathRef path = RCTPathCreateWithRoundedRect(bounds, cornerInsets, nil);
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = path;
CGPathRelease(path);
return maskLayer;
}
- (void)clearExistingGradientLayers
{
if (_gradientLayers == nil) {
_gradientLayers = [NSMutableArray new];
return;
}
for (CAGradientLayer *gradientLayer in _gradientLayers) {
[gradientLayer removeFromSuperlayer];
}
[_gradientLayers removeAllObjects];
}
#pragma mark - Accessibility
- (NSObject *)accessibilityElement
{
return self;
}
static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
{
NSMutableString *result = [NSMutableString stringWithString:@""];
for (UIView *subview in view.subviews) {
NSString *label = subview.accessibilityLabel;
if (!label) {
label = RCTRecursiveAccessibilityLabel(subview);
}
if (label && label.length > 0) {
if (result.length > 0) {
[result appendString:@" "];
}
[result appendString:label];
}
}
return result;
}
- (NSString *)accessibilityLabel
{
NSString *label = super.accessibilityLabel;
if (label) {
return label;
}
return RCTRecursiveAccessibilityLabel(self.currentContainerView);
}
- (BOOL)isAccessibilityElement
{
if (self.contentView != nil) {
return self.contentView.isAccessibilityElement;
}
return [super isAccessibilityElement];
}
- (NSString *)accessibilityValue
{
const auto &props = static_cast<const ViewProps &>(*_props);
const auto accessibilityState = props.accessibilityState.value_or(AccessibilityState{});
// Handle Switch.
if ((self.accessibilityTraits & AccessibilityTraitSwitch) == AccessibilityTraitSwitch) {
if (accessibilityState.checked == AccessibilityState::Checked) {
return @"1";
} else if (accessibilityState.checked == AccessibilityState::Unchecked) {
return @"0";
}
}
NSMutableArray *valueComponents = [NSMutableArray new];
NSString *roleString = (props.role != Role::None) ? [NSString stringWithUTF8String:toString(props.role).c_str()]
: [NSString stringWithUTF8String:props.accessibilityRole.c_str()];
// In iOS, checkbox and radio buttons aren't recognized as traits. However,
// because our apps use checkbox and radio buttons often, we should announce
// these to screenreader users. (They should already be familiar with them
// from using web).
if ([roleString isEqualToString:@"checkbox"]) {
[valueComponents addObject:RCTLocalizedString("checkbox", "checkable interactive control")];
}
if ([roleString isEqualToString:@"radio"]) {
[valueComponents
addObject:
RCTLocalizedString(
"radio button",
"a checkable input that when associated with other radio buttons, only one of which can be checked at a time")];
}
// Handle states which haven't already been handled.
if (accessibilityState.checked == AccessibilityState::Checked) {
[valueComponents
addObject:RCTLocalizedString("checked", "a checkbox, radio button, or other widget which is checked")];
}
if (accessibilityState.checked == AccessibilityState::Unchecked) {
[valueComponents
addObject:RCTLocalizedString("unchecked", "a checkbox, radio button, or other widget which is unchecked")];
}
if (accessibilityState.checked == AccessibilityState::Mixed) {
[valueComponents
addObject:RCTLocalizedString(
"mixed", "a checkbox, radio button, or other widget which is both checked and unchecked")];
}
if (accessibilityState.expanded.value_or(false)) {
[valueComponents
addObject:RCTLocalizedString("expanded", "a menu, dialog, accordian panel, or other widget which is expanded")];
}
if (accessibilityState.busy) {
[valueComponents addObject:RCTLocalizedString("busy", "an element currently being updated or modified")];
}
// Using super.accessibilityValue:
// 1. to access the value that is set to accessibilityValue in updateProps
// 2. can't access from self.accessibilityElement because it resolves to self
if (super.accessibilityValue) {
[valueComponents addObject:super.accessibilityValue];
}
if (valueComponents.count > 0) {
return [valueComponents componentsJoinedByString:@", "];
}
return nil;
}
#pragma mark - Accessibility Events
- (BOOL)shouldGroupAccessibilityChildren
{
return YES;
}
- (NSArray<UIAccessibilityCustomAction *> *)accessibilityCustomActions
{
const auto &accessibilityActions = _props->accessibilityActions;
if (accessibilityActions.empty()) {
return nil;
}
NSMutableArray<UIAccessibilityCustomAction *> *customActions = [NSMutableArray array];
for (const auto &accessibilityAction : accessibilityActions) {
[customActions
addObject:[[UIAccessibilityCustomAction alloc] initWithName:RCTNSStringFromString(accessibilityAction.name)
target:self
selector:@selector(didActivateAccessibilityCustomAction:)]];
}
return [customActions copy];
}
- (BOOL)accessibilityActivate
{
if (_eventEmitter && _props->onAccessibilityTap) {
_eventEmitter->onAccessibilityTap();
return YES;
} else {
return NO;
}
}
- (BOOL)accessibilityPerformMagicTap
{
if (_eventEmitter && _props->onAccessibilityMagicTap) {
_eventEmitter->onAccessibilityMagicTap();
return YES;
} else {
return NO;
}
}
- (BOOL)accessibilityPerformEscape
{
if (_eventEmitter && _props->onAccessibilityEscape) {
_eventEmitter->onAccessibilityEscape();
return YES;
} else {
return NO;
}
}
- (void)accessibilityIncrement
{
if (_eventEmitter && _props->onAccessibilityAction) {
_eventEmitter->onAccessibilityAction("increment");
}
}
- (void)accessibilityDecrement
{
if (_eventEmitter && _props->onAccessibilityAction) {
_eventEmitter->onAccessibilityAction("decrement");
}
}
- (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action
{
if (_eventEmitter && _props->onAccessibilityAction) {
_eventEmitter->onAccessibilityAction(RCTStringFromNSString(action.name));
return YES;
} else {
return NO;
}
}
- (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point
{
return _eventEmitter;
}
- (NSString *)componentViewName_DO_NOT_USE_THIS_IS_BROKEN
{
return RCTNSStringFromString([[self class] componentDescriptorProvider].name);
}
@end
#ifdef __cplusplus
extern "C" {
#endif
// Can't the import generated Plugin.h because plugins are not in this BUCK target
Class<RCTComponentViewProtocol> RCTViewCls(void);
#ifdef __cplusplus
}
#endif
Class<RCTComponentViewProtocol> RCTViewCls(void)
{
return RCTViewComponentView.class;
}