Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit db4801c

Browse files
[iOS] text input methods to only call updateEditState once (#19161)
1 parent f16a16e commit db4801c

File tree

2 files changed

+87
-74
lines changed

2 files changed

+87
-74
lines changed

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,8 @@ - (void)setTextInputClient:(int)client {
325325
_textInputClient = client;
326326
}
327327

328-
- (void)setTextInputState:(NSDictionary*)state {
328+
// Return true if the new input state needs to be synced back to the framework.
329+
- (BOOL)setTextInputState:(NSDictionary*)state {
329330
NSString* newText = state[@"text"];
330331
BOOL textChanged = ![self.text isEqualToString:newText];
331332
if (textChanged) {
@@ -356,8 +357,7 @@ - (void)setTextInputState:(NSDictionary*)state {
356357
selectedRange.length != oldSelectedRange.length) {
357358
needsEditingStateUpdate = YES;
358359
[self.inputDelegate selectionWillChange:self];
359-
[self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:selectedRange]
360-
updateEditingState:NO];
360+
[self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]];
361361
_selectionAffinity = _kTextAffinityDownstream;
362362
if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)])
363363
_selectionAffinity = _kTextAffinityUpstream;
@@ -367,10 +367,9 @@ - (void)setTextInputState:(NSDictionary*)state {
367367
if (textChanged) {
368368
[self.inputDelegate textDidChange:self];
369369
}
370-
if (needsEditingStateUpdate) {
371-
// For consistency with Android behavior, send an update to the framework.
372-
[self updateEditingState];
373-
}
370+
371+
// For consistency with Android behavior, send an update to the framework if anything changed.
372+
return needsEditingStateUpdate;
374373
}
375374

376375
- (NSRange)clampSelection:(NSRange)range forText:(NSString*)text {
@@ -401,11 +400,8 @@ - (UITextRange*)selectedTextRange {
401400
return [[_selectedTextRange copy] autorelease];
402401
}
403402

404-
- (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
405-
[self setSelectedTextRange:selectedTextRange updateEditingState:YES];
406-
}
407-
408-
- (void)setSelectedTextRange:(UITextRange*)selectedTextRange updateEditingState:(BOOL)update {
403+
// Change the range of selected text, without notifying the framework.
404+
- (void)setSelectedTextRangeLocal:(UITextRange*)selectedTextRange {
409405
if (_selectedTextRange != selectedTextRange) {
410406
UITextRange* oldSelectedRange = _selectedTextRange;
411407
if (self.hasText) {
@@ -416,12 +412,14 @@ - (void)setSelectedTextRange:(UITextRange*)selectedTextRange updateEditingState:
416412
_selectedTextRange = [selectedTextRange copy];
417413
}
418414
[oldSelectedRange release];
419-
420-
if (update)
421-
[self updateEditingState];
422415
}
423416
}
424417

418+
- (void)setSelectedTextRange:(UITextRange*)selectedTextRange {
419+
[self setSelectedTextRangeLocal:selectedTextRange];
420+
[self updateEditingState];
421+
}
422+
425423
- (id)insertDictationResultPlaceholder {
426424
return @"";
427425
}
@@ -440,26 +438,32 @@ - (NSString*)textInRange:(UITextRange*)range {
440438
return [self.text substringWithRange:textRange];
441439
}
442440

443-
- (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
444-
NSRange replaceRange = ((FlutterTextRange*)range).range;
441+
// Replace the text within the specified range with the given text,
442+
// without notifying the framework.
443+
- (void)replaceRangeLocal:(NSRange)range withText:(NSString*)text {
445444
NSRange selectedRange = _selectedTextRange.range;
445+
446446
// Adjust the text selection:
447447
// * reduce the length by the intersection length
448448
// * adjust the location by newLength - oldLength + intersectionLength
449-
NSRange intersectionRange = NSIntersectionRange(replaceRange, selectedRange);
450-
if (replaceRange.location <= selectedRange.location)
451-
selectedRange.location += text.length - replaceRange.length;
449+
NSRange intersectionRange = NSIntersectionRange(range, selectedRange);
450+
if (range.location <= selectedRange.location)
451+
selectedRange.location += text.length - range.length;
452452
if (intersectionRange.location != NSNotFound) {
453453
selectedRange.location += intersectionRange.length;
454454
selectedRange.length -= intersectionRange.length;
455455
}
456456

457-
[self.text replaceCharactersInRange:[self clampSelection:replaceRange forText:self.text]
457+
[self.text replaceCharactersInRange:[self clampSelection:range forText:self.text]
458458
withString:text];
459-
[self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:[self clampSelection:selectedRange
460-
forText:self.text]]
461-
updateEditingState:NO];
459+
[self setSelectedTextRangeLocal:[FlutterTextRange
460+
rangeWithNSRange:[self clampSelection:selectedRange
461+
forText:self.text]]];
462+
}
462463

464+
- (void)replaceRange:(UITextRange*)range withText:(NSString*)text {
465+
NSRange replaceRange = ((FlutterTextRange*)range).range;
466+
[self replaceRangeLocal:replaceRange withText:text];
463467
[self updateEditingState];
464468
}
465469

@@ -522,11 +526,11 @@ - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelecte
522526

523527
if (markedTextRange.length > 0) {
524528
// Replace text in the marked range with the new text.
525-
[self replaceRange:self.markedTextRange withText:markedText];
529+
[self replaceRangeLocal:markedTextRange withText:markedText];
526530
markedTextRange.length = markedText.length;
527531
} else {
528532
// Replace text in the selected range with the new text.
529-
[self replaceRange:_selectedTextRange withText:markedText];
533+
[self replaceRangeLocal:selectedRange withText:markedText];
530534
markedTextRange = NSMakeRange(selectedRange.location, markedText.length);
531535
}
532536

@@ -535,9 +539,10 @@ - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelecte
535539

536540
NSUInteger selectionLocation = markedSelectedRange.location + markedTextRange.location;
537541
selectedRange = NSMakeRange(selectionLocation, markedSelectedRange.length);
538-
[self setSelectedTextRange:[FlutterTextRange rangeWithNSRange:[self clampSelection:selectedRange
539-
forText:self.text]]
540-
updateEditingState:YES];
542+
[self setSelectedTextRangeLocal:[FlutterTextRange
543+
rangeWithNSRange:[self clampSelection:selectedRange
544+
forText:self.text]]];
545+
[self updateEditingState];
541546
}
542547

543548
- (void)unmarkText {
@@ -1002,7 +1007,9 @@ + (void)setupInputView:(FlutterTextInputView*)inputView
10021007
}
10031008

10041009
- (void)setTextInputEditingState:(NSDictionary*)state {
1005-
[_activeView setTextInputState:state];
1010+
if ([_activeView setTextInputState:state]) {
1011+
[_activeView updateEditingState];
1012+
}
10061013
}
10071014

10081015
- (void)clearTextInputClient {

shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,27 @@ @interface FlutterTextInputView ()
1717
- (void)setTextInputState:(NSDictionary*)state;
1818
@end
1919

20-
@implementation FlutterTextInputPluginTest
21-
- (void)testSecureInput {
22-
// Setup test.
23-
id engine = OCMClassMock([FlutterEngine class]);
24-
FlutterTextInputPlugin* textInputPlugin = [[FlutterTextInputPlugin alloc] init];
20+
@implementation FlutterTextInputPluginTest {
21+
id engine;
22+
FlutterTextInputPlugin* textInputPlugin;
23+
}
24+
25+
- (void)setUp {
26+
[super setUp];
27+
28+
engine = OCMClassMock([FlutterEngine class]);
29+
textInputPlugin = [[FlutterTextInputPlugin alloc] init];
2530
textInputPlugin.textInputDelegate = engine;
31+
}
2632

33+
- (void)tearDown {
34+
[engine stopMocking];
35+
[[[[textInputPlugin textInputView] superview] subviews]
36+
makeObjectsPerformSelector:@selector(removeFromSuperview)];
37+
[super tearDown];
38+
}
39+
40+
- (void)testSecureInput {
2741
NSDictionary* config = @{
2842
@"inputType" : @{@"name" : @"TextInuptType.text"},
2943
@"keyboardAppearance" : @"Brightness.light",
@@ -61,17 +75,9 @@ - (void)testSecureInput {
6175
// The one FlutterTextInputView we inserted into the view hierarchy should be the text input
6276
// plugin's active text input view.
6377
XCTAssertEqual(inputView, textInputPlugin.textInputView);
64-
65-
// Clean up.
66-
[engine stopMocking];
67-
[[[[textInputPlugin textInputView] superview] subviews]
68-
makeObjectsPerformSelector:@selector(removeFromSuperview)];
6978
}
7079

7180
- (void)testTextChangesTriggerUpdateEditingClient {
72-
// Setup test.
73-
id engine = OCMClassMock([FlutterEngine class]);
74-
7581
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
7682
inputView.textInputDelegate = engine;
7783

@@ -86,15 +92,9 @@ - (void)testTextChangesTriggerUpdateEditingClient {
8692
// Don't send anything if there's nothing new.
8793
[inputView setTextInputState:@{@"text" : @"AFTER"}];
8894
OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]);
89-
90-
// Clean up.
91-
[engine stopMocking];
9295
}
9396

9497
- (void)testSelectionChangeTriggersUpdateEditingClient {
95-
// Setup test.
96-
id engine = OCMClassMock([FlutterEngine class]);
97-
9898
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
9999
inputView.textInputDelegate = engine;
100100

@@ -118,15 +118,9 @@ - (void)testSelectionChangeTriggersUpdateEditingClient {
118118
[inputView
119119
setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}];
120120
OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]);
121-
122-
// Clean up.
123-
[engine stopMocking];
124121
}
125122

126123
- (void)testComposingChangeTriggersUpdateEditingClient {
127-
// Setup test.
128-
id engine = OCMClassMock([FlutterEngine class]);
129-
130124
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
131125
inputView.textInputDelegate = engine;
132126

@@ -151,17 +145,9 @@ - (void)testComposingChangeTriggersUpdateEditingClient {
151145
[inputView
152146
setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
153147
OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]);
154-
155-
// Clean up.
156-
[engine stopMocking];
157148
}
158149

159150
- (void)testAutofillInputViews {
160-
// Setup test.
161-
id engine = OCMClassMock([FlutterEngine class]);
162-
FlutterTextInputPlugin* textInputPlugin = [[FlutterTextInputPlugin alloc] init];
163-
textInputPlugin.textInputDelegate = engine;
164-
165151
NSDictionary* template = @{
166152
@"inputType" : @{@"name" : @"TextInuptType.text"},
167153
@"keyboardAppearance" : @"Brightness.light",
@@ -214,26 +200,15 @@ - (void)testAutofillInputViews {
214200

215201
// Verify behavior.
216202
OCMVerify([engine updateEditingClient:0 withState:[OCMArg isNotNil] withTag:@"field2"]);
217-
218-
// Clean up.
219-
[engine stopMocking];
220-
[[[[textInputPlugin textInputView] superview] subviews]
221-
makeObjectsPerformSelector:@selector(removeFromSuperview)];
222203
}
223204

224205
- (void)testAutocorrectionPromptRectAppears {
225-
// Setup test.
226-
id engine = OCMClassMock([FlutterEngine class]);
227-
228206
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithFrame:CGRectZero];
229207
inputView.textInputDelegate = engine;
230208
[inputView firstRectForRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]];
231209

232210
// Verify behavior.
233211
OCMVerify([engine showAutocorrectionPromptRectForStart:0 end:1 withClient:0]);
234-
235-
// Clean up mocks
236-
[engine stopMocking];
237212
}
238213

239214
- (void)testTextRangeFromPositionMatchesUITextViewBehavior {
@@ -248,4 +223,35 @@ - (void)testTextRangeFromPositionMatchesUITextViewBehavior {
248223
XCTAssertEqual(range.location, 0);
249224
XCTAssertEqual(range.length, 2);
250225
}
226+
227+
- (void)testUITextInputCallsUpdateEditingStateOnce {
228+
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
229+
inputView.textInputDelegate = engine;
230+
231+
__block int updateCount = 0;
232+
OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]])
233+
.andDo(^(NSInvocation* invocation) {
234+
updateCount++;
235+
});
236+
237+
[inputView insertText:@"text to insert"];
238+
// Update the framework exactly once.
239+
XCTAssertEqual(updateCount, 1);
240+
241+
[inputView deleteBackward];
242+
XCTAssertEqual(updateCount, 2);
243+
244+
inputView.selectedTextRange = [FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)];
245+
XCTAssertEqual(updateCount, 3);
246+
247+
[inputView replaceRange:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 1)]
248+
withText:@"replace text"];
249+
XCTAssertEqual(updateCount, 4);
250+
251+
[inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)];
252+
XCTAssertEqual(updateCount, 5);
253+
254+
[inputView unmarkText];
255+
XCTAssertEqual(updateCount, 6);
256+
}
251257
@end

0 commit comments

Comments
 (0)