iOS Text Editing Infinite Loop (#20160)
Fixes an infinite loop by eliminating an unnecessary engine/framework message.
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
index 71f5354..507b9ca 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
@@ -537,10 +537,7 @@
_textInputClient = 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 {
+- (void)setTextInputState:(NSDictionary*)state {
NSString* newText = state[@"text"];
BOOL textChanged = ![self.text isEqualToString:newText];
if (textChanged) {
@@ -575,9 +572,6 @@
if (textChanged) {
[self.inputDelegate textDidChange:self];
}
-
- // 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.
@@ -1423,9 +1417,7 @@
}
- (void)setTextInputEditingState:(NSDictionary*)state {
- if ([_activeView setTextInputState:state]) {
- [_activeView updateEditingState];
- }
+ [_activeView setTextInputState:state];
}
- (void)clearTextInputClient {
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m
index 432ce71..88683ab 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m
@@ -16,7 +16,7 @@
@property(nonatomic, copy) NSString* autofillId;
- (void)setEditableTransform:(NSArray*)matrix;
-- (BOOL)setTextInputState:(NSDictionary*)state;
+- (void)setTextInputState:(NSDictionary*)state;
- (void)setMarkedRect:(CGRect)markedRect;
- (void)updateEditingState;
- (BOOL)isVisibleToAutofill;
@@ -211,71 +211,45 @@
XCTAssertEqual(updateCount, 6);
}
-- (void)testTextChangesTriggerUpdateEditingClient {
+- (void)testTextChangesDoNotTriggerUpdateEditingClient {
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
inputView.textInputDelegate = engine;
+ __block int updateCount = 0;
+ OCMStub([engine updateEditingClient:0 withState:[OCMArg isNotNil]])
+ .andDo(^(NSInvocation* invocation) {
+ updateCount++;
+ });
+
[inputView.text setString:@"BEFORE"];
+ XCTAssertEqual(updateCount, 0);
+
inputView.markedTextRange = nil;
inputView.selectedTextRange = nil;
+ XCTAssertEqual(updateCount, 1);
- // Text changes trigger update.
- XCTAssertTrue([inputView setTextInputState:@{@"text" : @"AFTER"}]);
+ // Text changes don't trigger an update.
+ XCTAssertEqual(updateCount, 1);
+ [inputView setTextInputState:@{@"text" : @"AFTER"}];
+ XCTAssertEqual(updateCount, 1);
+ [inputView setTextInputState:@{@"text" : @"AFTER"}];
+ XCTAssertEqual(updateCount, 1);
- // Don't send anything if there's nothing new.
- XCTAssertFalse([inputView setTextInputState:@{@"text" : @"AFTER"}]);
-}
-
-- (void)testSelectionChangeDoesNotTriggerUpdateEditingClient {
- FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
- inputView.textInputDelegate = engine;
-
- [inputView.text setString:@"SELECTION"];
- inputView.markedTextRange = nil;
- inputView.selectedTextRange = nil;
-
- BOOL shouldUpdate = [inputView
+ // Selection changes don't trigger an update.
+ [inputView
setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @0, @"selectionExtent" : @3}];
- XCTAssertFalse(shouldUpdate);
-
- shouldUpdate = [inputView
+ XCTAssertEqual(updateCount, 1);
+ [inputView
setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @3}];
- XCTAssertFalse(shouldUpdate);
+ XCTAssertEqual(updateCount, 1);
- shouldUpdate = [inputView
- setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}];
- XCTAssertFalse(shouldUpdate);
-
- // Don't send anything if there's nothing new.
- shouldUpdate = [inputView
- setTextInputState:@{@"text" : @"SELECTION", @"selectionBase" : @1, @"selectionExtent" : @2}];
- XCTAssertFalse(shouldUpdate);
-}
-
-- (void)testComposingChangeDoesNotTriggerUpdateEditingClient {
- FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
- inputView.textInputDelegate = engine;
-
- // Reset to test marked text.
- [inputView.text setString:@"COMPOSING"];
- inputView.markedTextRange = nil;
- inputView.selectedTextRange = nil;
-
- BOOL shouldUpdate = [inputView
- setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @0, @"composingExtent" : @3}];
- XCTAssertFalse(shouldUpdate);
-
- shouldUpdate = [inputView
+ // Composing region changes don't trigger an update.
+ [inputView
+ setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
+ XCTAssertEqual(updateCount, 1);
+ [inputView
setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @3}];
- XCTAssertFalse(shouldUpdate);
-
- shouldUpdate = [inputView
- setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
- XCTAssertFalse(shouldUpdate);
-
- shouldUpdate = [inputView
- setTextInputState:@{@"text" : @"COMPOSING", @"composingBase" : @1, @"composingExtent" : @2}];
- XCTAssertFalse(shouldUpdate);
+ XCTAssertEqual(updateCount, 1);
}
- (void)testUITextInputAvoidUnnecessaryUndateEditingClientCalls {