diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 462d6f5234e91..71f5354c4b64d 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -538,6 +538,8 @@ - (void)setTextInputClient:(int)client { } // Return true if the new input state needs to be synced back to the framework. +// TODO(LongCatIsLooong): setTextInputState should never call updateEditingState. Sending the +// editing value back may overwrite the framework's updated editing value. - (BOOL)setTextInputState:(NSDictionary*)state { NSString* newText = state[@"text"]; BOOL textChanged = ![self.text isEqualToString:newText]; @@ -545,19 +547,14 @@ - (BOOL)setTextInputState:(NSDictionary*)state { [self.inputDelegate textWillChange:self]; [self.text setString:newText]; } - BOOL needsEditingStateUpdate = textChanged; NSInteger composingBase = [state[@"composingBase"] intValue]; NSInteger composingExtent = [state[@"composingExtent"] intValue]; NSRange composingRange = [self clampSelection:NSMakeRange(MIN(composingBase, composingExtent), ABS(composingBase - composingExtent)) forText:self.text]; - FlutterTextRange* newMarkedRange = + + self.markedTextRange = composingRange.length > 0 ? [FlutterTextRange rangeWithNSRange:composingRange] : nil; - needsEditingStateUpdate = - needsEditingStateUpdate || - (!newMarkedRange ? self.markedTextRange != nil - : ![newMarkedRange isEqualTo:(FlutterTextRange*)self.markedTextRange]); - self.markedTextRange = newMarkedRange; NSRange selectedRange = [self clampSelectionFromBase:[state[@"selectionBase"] intValue] extent:[state[@"selectionExtent"] intValue] @@ -565,7 +562,6 @@ - (BOOL)setTextInputState:(NSDictionary*)state { NSRange oldSelectedRange = [(FlutterTextRange*)self.selectedTextRange range]; if (!NSEqualRanges(selectedRange, oldSelectedRange)) { - needsEditingStateUpdate = YES; [self.inputDelegate selectionWillChange:self]; [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]]; @@ -580,8 +576,8 @@ - (BOOL)setTextInputState:(NSDictionary*)state { [self.inputDelegate textDidChange:self]; } - // For consistency with Android behavior, send an update to the framework if anything changed. - return needsEditingStateUpdate; + // For consistency with Android behavior, send an update to the framework if the text changed. + return textChanged; } // Extracts the selection information from the editing state dictionary. @@ -788,6 +784,8 @@ - (void)setMarkedText:(NSString*)markedText selectedRange:(NSRange)markedSelecte } - (void)unmarkText { + if (!self.markedTextRange) + return; self.markedTextRange = nil; [self updateEditingState]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m index 3936e17eb2a21..432ce718bf31a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m @@ -226,7 +226,7 @@ - (void)testTextChangesTriggerUpdateEditingClient { XCTAssertFalse([inputView setTextInputState:@{@"text" : @"AFTER"}]); } -- (void)testSelectionChangeTriggersUpdateEditingClient { +- (void)testSelectionChangeDoesNotTriggerUpdateEditingClient { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine; @@ -236,15 +236,15 @@ - (void)testSelectionChangeTriggersUpdateEditingClient { BOOL shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}]; - XCTAssertTrue(shouldUpdate); + XCTAssertFalse(shouldUpdate); shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}]; - XCTAssertTrue(shouldUpdate); + XCTAssertFalse(shouldUpdate); shouldUpdate = [inputView setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}]; - XCTAssertTrue(shouldUpdate); + XCTAssertFalse(shouldUpdate); // Don't send anything if there's nothing new. shouldUpdate = [inputView @@ -252,7 +252,7 @@ - (void)testSelectionChangeTriggersUpdateEditingClient { XCTAssertFalse(shouldUpdate); } -- (void)testComposingChangeTriggersUpdateEditingClient { +- (void)testComposingChangeDoesNotTriggerUpdateEditingClient { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine; @@ -263,22 +263,44 @@ - (void)testComposingChangeTriggersUpdateEditingClient { BOOL shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @0, @"composingExtent" : @3}]; - XCTAssertTrue(shouldUpdate); + XCTAssertFalse(shouldUpdate); shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}]; - XCTAssertTrue(shouldUpdate); + XCTAssertFalse(shouldUpdate); shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}]; - XCTAssertTrue(shouldUpdate); + XCTAssertFalse(shouldUpdate); - // Don't send anything if there's nothing new. shouldUpdate = [inputView setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}]; XCTAssertFalse(shouldUpdate); } +- (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; + inputView.textInputDelegate = engine; + + __block int updateCount = 0; + OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]]) + .andDo(^(NSInvocation* invocation) { + updateCount++; + }); + + [inputView unmarkText]; + // updateEditingClient shouldn't fire as the text is already unmarked. + XCTAssertEqual(updateCount, 0); + + [inputView setMarkedText:@"marked text" selectedRange:NSMakeRange(0, 1)]; + // updateEditingClient fires in response to setMarkedText. + XCTAssertEqual(updateCount, 1); + + [inputView unmarkText]; + // updateEditingClient fires in response to unmarkText. + XCTAssertEqual(updateCount, 2); +} + - (void)testUpdateEditingClientNegativeSelection { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init]; inputView.textInputDelegate = engine;