Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix cursor moving while typing quickly and autocorrection triggered in controlled single line TextInput on iOS (New Arch) #46970

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, assign, readonly) CGFloat zoomScale;
@property (nonatomic, assign, readonly) CGPoint contentOffset;
@property (nonatomic, assign, readonly) UIEdgeInsets contentInset;
@property (nullable, nonatomic, copy) NSDictionary<NSAttributedStringKey, id> *typingAttributes;

// This protocol disallows direct access to `selectedTextRange` property because
// unwise usage of it can break the `delegate` behavior. So, we always have to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

#import <react/renderer/components/iostextinput/TextInputComponentDescriptor.h>
#import <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>
#import <react/renderer/textlayoutmanager/RCTTextPrimitivesConversions.h>
#import <react/renderer/textlayoutmanager/TextLayoutManager.h>

#import <React/RCTBackedTextInputViewProtocol.h>
Expand Down Expand Up @@ -61,6 +62,13 @@ @implementation RCTTextInputComponentView {
*/
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> *_defaultTypingAttributes;
}

#pragma mark - UIView overrides
Expand All @@ -79,11 +87,27 @@ - (instancetype)initWithFrame:(CGRect)frame

[self addSubview:_backedTextInputView];
[self initializeReturnKeyType];

_defaultTypingAttributes = [_backedTextInputView.typingAttributes copy];
}

return self;
}

- (void)updateEventEmitter:(const EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];

NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
[_backedTextInputView.defaultTextAttributes mutableCopy];

RCTWeakEventEmitterWrapper *eventEmitterWrapper = [RCTWeakEventEmitterWrapper new];
eventEmitterWrapper.eventEmitter = _eventEmitter;
defaultAttributes[RCTAttributedStringEventEmitterKey] = eventEmitterWrapper;

_backedTextInputView.defaultTextAttributes = defaultAttributes;
}

- (void)didMoveToWindow
{
[super didMoveToWindow];
Expand Down Expand Up @@ -236,8 +260,11 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared &
}

if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) {
_backedTextInputView.defaultTextAttributes =
NSMutableDictionary<NSAttributedStringKey, id> *defaultAttributes =
RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes(RCTFontSizeMultiplier()));
defaultAttributes[RCTAttributedStringEventEmitterKey] =
_backedTextInputView.defaultTextAttributes[RCTAttributedStringEventEmitterKey];
_backedTextInputView.defaultTextAttributes = [defaultAttributes copy];
}

if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) {
Expand Down Expand Up @@ -418,6 +445,7 @@ - (void)textInputDidChange

- (void)textInputDidChangeSelection
{
[self _updateTypingAttributes];
if (_comingFromJS) {
return;
}
Expand Down Expand Up @@ -674,9 +702,26 @@ - (void)_setAttributedString:(NSAttributedString *)attributedString
[_backedTextInputView scrollRangeToVisible:NSMakeRange(offsetStart, 0)];
}
[self _restoreTextSelection];
[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) {
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)_setMultiline:(BOOL)multiline
{
[_backedTextInputView removeFromSuperview];
Expand Down Expand Up @@ -706,6 +751,10 @@ - (void)_setShowSoftInputOnFocus:(BOOL)showSoftInputOnFocus

- (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldText
{
if (![newText.string isEqualToString:oldText.string]) {
return NO;
}

// 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.
Expand All @@ -732,10 +781,107 @@ - (BOOL)_textOf:(NSAttributedString *)newText equals:(NSAttributedString *)oldTe
_backedTextInputView.markedTextRange || _backedTextInputView.isSecureTextEntry || fontHasBeenUpdatedBySystem;

if (shouldFallbackToBareTextComparison) {
return ([newText.string isEqualToString:oldText.string]);
return YES;
} else {
return ([newText isEqualToAttributedString:oldText]);
return [self _areAttributesEffectivelyEqual:oldText newText:newText];
}
}

- (BOOL)_areAttributesEffectivelyEqual:(NSAttributedString *)oldText newText:(NSAttributedString *)newText
{
// We check that for every fragment in the old string
// 1. A fragment of the same range exists in the new string
// 2. The attributes of each matching fragment are the same, ignoring those which match the always set default typing
// attributes
__block BOOL areAttriubtesEqual = YES;
[oldText enumerateAttributesInRange:NSMakeRange(0, oldText.length)
options:0
usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange range, BOOL *stop) {
[newText
enumerateAttributesInRange:range
options:0
usingBlock:^(
NSDictionary<NSAttributedStringKey, id> *innerAttrs,
NSRange innerRange,
BOOL *innerStop) {
if (!NSEqualRanges(range, innerRange)) {
areAttriubtesEqual = NO;
*innerStop = YES;
*stop = YES;
return;
}

NSMutableDictionary<NSAttributedStringKey, id> *normAttrs =
[attrs mutableCopy];
NSMutableDictionary<NSAttributedStringKey, id> *normInnerAttrs =
[innerAttrs mutableCopy];

__unused NSDictionary *currentTypingAttributes =
_backedTextInputView.typingAttributes;
__unused NSDictionary *defaultAttributes =
_backedTextInputView.defaultTextAttributes;

for (NSAttributedStringKey key in _defaultTypingAttributes) {
id defaultAttr = _defaultTypingAttributes[key];
if ([normAttrs[key] isEqual:defaultAttr] ||
(key == NSParagraphStyleAttributeName &&
[self _areParagraphStylesEffectivelyEqual:normAttrs[key]
other:defaultAttr])) {
[normAttrs removeObjectForKey:key];
}
if ([normInnerAttrs[key] isEqual:defaultAttr] ||
(key == NSParagraphStyleAttributeName &&
[self _areParagraphStylesEffectivelyEqual:normInnerAttrs[key]
other:defaultAttr])) {
[normInnerAttrs removeObjectForKey:key];
}
}

if (![normAttrs isEqualToDictionary:normInnerAttrs]) {
areAttriubtesEqual = NO;
*innerStop = YES;
*stop = YES;
}
}];
}];

return areAttriubtesEqual;
}

// The default NSParagraphStyle included as part of typingAttributes will eventually resolve "natural" directions to
// physical direction, so we should compare resolved directions
- (BOOL)_areParagraphStylesEffectivelyEqual:(NSParagraphStyle *)style1 other:(NSParagraphStyle *)style2
{
NSMutableParagraphStyle *mutableStyle1 = [style1 mutableCopy];
NSMutableParagraphStyle *mutableStyle2 = [style2 mutableCopy];

const auto &textAttributes = static_cast<const TextInputProps &>(*_props).textAttributes;

auto layoutDirection = textAttributes.layoutDirection.value_or(LayoutDirection::LeftToRight);
if (mutableStyle1.alignment == NSTextAlignmentNatural) {
mutableStyle1.alignment =
layoutDirection == LayoutDirection::LeftToRight ? NSTextAlignmentLeft : NSTextAlignmentRight;
}
if (mutableStyle2.alignment == NSTextAlignmentNatural) {
mutableStyle2.alignment =
layoutDirection == LayoutDirection::LeftToRight ? NSTextAlignmentLeft : NSTextAlignmentRight;
}

auto baseWritingDirection = [&]() {
if (textAttributes.baseWritingDirection.has_value()) {
return RCTNSWritingDirectionFromWritingDirection(textAttributes.baseWritingDirection.value());
} else {
return [NSParagraphStyle defaultWritingDirectionForLanguage:nil];
}
}();
if (mutableStyle1.baseWritingDirection == NSWritingDirectionNatural) {
mutableStyle1.baseWritingDirection = baseWritingDirection;
}
if (mutableStyle2.baseWritingDirection == NSWritingDirectionNatural) {
mutableStyle2.baseWritingDirection = baseWritingDirection;
}

return [mutableStyle1 isEqual:mutableStyle2];
}

- (SubmitBehavior)getSubmitBehavior
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ NSString *const RCTTextAttributesAccessibilityRoleAttributeName = @"Accessibilit
/*
* Creates `NSTextAttributes` from given `facebook::react::TextAttributes`
*/
NSDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(
NSMutableDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(
const facebook::react::TextAttributes &textAttributes);

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ - (void)dealloc
_weakEventEmitter.reset();
}

- (BOOL)isEqual:(id)object
{
// We consider the underlying EventEmitter as the identity
if (![object isKindOfClass:[self class]]) {
return NO;
}

auto thisEventEmitter = [self eventEmitter];
auto otherEventEmitter = [((RCTWeakEventEmitterWrapper *)object) eventEmitter];
return thisEventEmitter == otherEventEmitter;
}

- (NSUInteger)hash
{
// We consider the underlying EventEmitter as the identity
return (NSUInteger)_weakEventEmitter.lock().get();
}

@end

inline static UIFontWeight RCTUIFontWeightFromInteger(NSInteger fontWeight)
Expand Down Expand Up @@ -156,7 +174,8 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex

inline static UIColor *RCTEffectiveForegroundColorFromTextAttributes(const TextAttributes &textAttributes)
{
UIColor *effectiveForegroundColor = RCTUIColorFromSharedColor(textAttributes.foregroundColor) ?: [UIColor blackColor];
UIColor *effectiveForegroundColor =
RCTPlatformColorFromColor(*textAttributes.foregroundColor) ?: [UIColor blackColor];

if (!isnan(textAttributes.opacity)) {
effectiveForegroundColor = [effectiveForegroundColor
Expand All @@ -168,7 +187,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex

inline static UIColor *RCTEffectiveBackgroundColorFromTextAttributes(const TextAttributes &textAttributes)
{
UIColor *effectiveBackgroundColor = RCTUIColorFromSharedColor(textAttributes.backgroundColor);
UIColor *effectiveBackgroundColor = RCTPlatformColorFromColor(*textAttributes.backgroundColor);

if (effectiveBackgroundColor && !isnan(textAttributes.opacity)) {
effectiveBackgroundColor = [effectiveBackgroundColor
Expand All @@ -178,7 +197,8 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex
return effectiveBackgroundColor ?: [UIColor clearColor];
}

NSDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(const TextAttributes &textAttributes)
NSMutableDictionary<NSAttributedStringKey, id> *RCTNSTextAttributesFromTextAttributes(
const TextAttributes &textAttributes)
{
NSMutableDictionary<NSAttributedStringKey, id> *attributes = [NSMutableDictionary dictionaryWithCapacity:10];

Expand Down Expand Up @@ -256,7 +276,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex
NSUnderlineStyle style = RCTNSUnderlineStyleFromTextDecorationStyle(
textAttributes.textDecorationStyle.value_or(TextDecorationStyle::Solid));

UIColor *textDecorationColor = RCTUIColorFromSharedColor(textAttributes.textDecorationColor);
UIColor *textDecorationColor = RCTPlatformColorFromColor(*textAttributes.textDecorationColor);

// Underline
if (textDecorationLineType == TextDecorationLineType::Underline ||
Expand Down Expand Up @@ -285,7 +305,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex
NSShadow *shadow = [NSShadow new];
shadow.shadowOffset = CGSize{textShadowOffset.width, textShadowOffset.height};
shadow.shadowBlurRadius = textAttributes.textShadowRadius;
shadow.shadowColor = RCTUIColorFromSharedColor(textAttributes.textShadowColor);
shadow.shadowColor = RCTPlatformColorFromColor(*textAttributes.textShadowColor);
attributes[NSShadowAttributeName] = shadow;
}

Expand All @@ -302,7 +322,7 @@ inline static CGFloat RCTEffectiveFontSizeMultiplierFromTextAttributes(const Tex
attributes[RCTTextAttributesAccessibilityRoleAttributeName] = [NSString stringWithUTF8String:roleStr.c_str()];
}

return [attributes copy];
return attributes;
}

void RCTApplyBaselineOffset(NSMutableAttributedString *attributedText)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

#import <UIKit/UIKit.h>

#include <react/renderer/graphics/RCTPlatformColorUtils.h>
#include <react/renderer/textlayoutmanager/RCTFontProperties.h>
#include <react/renderer/textlayoutmanager/RCTFontUtils.h>
#import <react/renderer/graphics/RCTPlatformColorUtils.h>
#import <react/renderer/textlayoutmanager/RCTFontProperties.h>
#import <react/renderer/textlayoutmanager/RCTFontUtils.h>

inline static NSTextAlignment RCTNSTextAlignmentFromTextAlignment(facebook::react::TextAlignment textAlignment)
{
Expand Down Expand Up @@ -112,9 +112,3 @@ inline static NSUnderlineStyle RCTNSUnderlineStyleFromTextDecorationStyle(
return NSUnderlinePatternDot | NSUnderlineStyleSingle;
}
}

// TODO: this file has some duplicates method, we can remove it
inline static UIColor *_Nullable RCTUIColorFromSharedColor(const facebook::react::SharedColor &sharedColor)
{
return RCTPlatformColorFromColor(*sharedColor);
}
Loading