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 {