Skip to content

Commit 282baa0

Browse files
committed
Fix iOS hidden caret Metal errors
1 parent d6ba88e commit 282baa0

3 files changed

Lines changed: 95 additions & 4 deletions

File tree

‎packages/react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.mm‎

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ @implementation RCTUITextField {
1717
NSArray<UIBarButtonItemGroup *> *_initialValueLeadingBarButtonGroups;
1818
NSArray<UIBarButtonItemGroup *> *_initialValueTrailingBarButtonGroups;
1919
NSArray<NSString *> *_acceptDragAndDropTypes;
20+
// Keep the requested tint while a transparent tint hides the caret.
21+
UIColor *_requestedTintColor;
2022
}
2123

2224
// This should not be needed but internal build were failing without it.
@@ -210,13 +212,31 @@ - (void)removeDictationResultPlaceholder:(id)placeholder willInsertResult:(BOOL)
210212

211213
#pragma mark - Caret Manipulation
212214

213-
- (CGRect)caretRectForPosition:(UITextPosition *)position
215+
// Returning CGRectZero for a hidden caret causes oversized Metal surface errors on iOS
216+
// 17. A transparent tint hides the caret without changing its bounds. Restore the tint
217+
// for non-empty selections to preserve the selection highlight and handles.
218+
- (void)setCaretHidden:(BOOL)caretHidden
214219
{
215-
if (_caretHidden) {
216-
return CGRectZero;
220+
if (_caretHidden == caretHidden) {
221+
return;
217222
}
218223

219-
return [super caretRectForPosition:position];
224+
_caretHidden = caretHidden;
225+
[self _updateAppliedTintColor];
226+
}
227+
228+
// Do not override the getter. UIKit reads the applied caret color through `-tintColor`.
229+
- (void)setTintColor:(UIColor *)tintColor
230+
{
231+
_requestedTintColor = tintColor;
232+
[self _updateAppliedTintColor];
233+
}
234+
235+
- (void)_updateAppliedTintColor
236+
{
237+
UITextRange *selectedTextRange = self.selectedTextRange;
238+
BOOL shouldHideCaret = _caretHidden && (selectedTextRange == nil || selectedTextRange.isEmpty);
239+
[super setTintColor:shouldHideCaret ? [UIColor clearColor] : _requestedTintColor];
220240
}
221241

222242
#pragma mark - Positioning Overrides
@@ -239,6 +259,7 @@ - (CGRect)editingRectForBounds:(CGRect)bounds
239259
- (void)setSelectedTextRange:(UITextRange *)selectedTextRange
240260
{
241261
[super setSelectedTextRange:selectedTextRange];
262+
[self _updateAppliedTintColor];
242263
[_textInputDelegateAdapter selectedTextRangeWasSet];
243264
}
244265
#pragma clang diagnostic pop
@@ -252,6 +273,7 @@ - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BO
252273
}
253274

254275
[super setSelectedTextRange:selectedTextRange];
276+
[self _updateAppliedTintColor];
255277
}
256278

257279
- (void)scrollRangeToVisible:(NSRange)range

‎packages/react-native/React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,10 @@ - (void)_setMultiline:(BOOL)multiline
849849
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new];
850850
backedTextInputView.frame = _backedTextInputView.frame;
851851
RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView);
852+
// The copied tint can be transparent when the source hides its caret. Restore the
853+
// selection color from props after changing the input type.
854+
backedTextInputView.tintColor =
855+
RCTUIColorFromSharedColor(static_cast<const TextInputProps &>(*_props).selectionColor);
852856
_backedTextInputView = backedTextInputView;
853857
[self addSubview:_backedTextInputView];
854858
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
#import <React/RCTUITextField.h>
9+
#import <XCTest/XCTest.h>
10+
11+
@interface RCTUITextFieldTests : XCTestCase
12+
@end
13+
14+
@implementation RCTUITextFieldTests
15+
16+
- (void)testCaretHiddenMakesTheCaretTransparent
17+
{
18+
RCTUITextField *textField = [RCTUITextField new];
19+
textField.tintColor = UIColor.redColor;
20+
21+
textField.caretHidden = YES;
22+
23+
XCTAssertEqualObjects(textField.tintColor, UIColor.clearColor);
24+
}
25+
26+
- (void)testClearingCaretHiddenRestoresTheSelectionColor
27+
{
28+
RCTUITextField *textField = [RCTUITextField new];
29+
textField.tintColor = UIColor.redColor;
30+
textField.caretHidden = YES;
31+
32+
textField.caretHidden = NO;
33+
34+
XCTAssertEqualObjects(textField.tintColor, UIColor.redColor);
35+
}
36+
37+
- (void)testSelectionColorSetWhileCaretHiddenIsAppliedOnceTheCaretIsShown
38+
{
39+
RCTUITextField *textField = [RCTUITextField new];
40+
textField.caretHidden = YES;
41+
42+
textField.tintColor = UIColor.redColor;
43+
44+
XCTAssertEqualObjects(textField.tintColor, UIColor.clearColor);
45+
46+
textField.caretHidden = NO;
47+
48+
XCTAssertEqualObjects(textField.tintColor, UIColor.redColor);
49+
}
50+
51+
- (void)testSelectionColorIsAppliedToANonEmptySelection
52+
{
53+
RCTUITextField *textField = [RCTUITextField new];
54+
textField.attributedText = [[NSAttributedString alloc] initWithString:@"Hello"];
55+
textField.tintColor = UIColor.redColor;
56+
textField.caretHidden = YES;
57+
58+
UITextPosition *start = textField.beginningOfDocument;
59+
UITextPosition *end = [textField positionFromPosition:start offset:textField.attributedText.length];
60+
[textField setSelectedTextRange:[textField textRangeFromPosition:start toPosition:end] notifyDelegate:NO];
61+
62+
XCTAssertEqualObjects(textField.tintColor, UIColor.redColor);
63+
}
64+
65+
@end

0 commit comments

Comments
 (0)