Fix loss of negative text selection ranges (#19785)

iOS now matches Android behavior in that empty selections put the cursor at the start of the field.
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
index dc85fbe..7130469 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
@@ -357,7 +357,21 @@
       selectedRange.length != oldSelectedRange.length) {
     needsEditingStateUpdate = YES;
     [self.inputDelegate selectionWillChange:self];
-    [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]];
+
+    // The state may contain an invalid selection, such as when no selection was
+    // explicitly set in the framework. This is handled here by setting the
+    // selection to (0,0). In contrast, Android handles this situation by
+    // clearing the selection, but the result in both cases is that the cursor
+    // is placed at the beginning of the field.
+    bool selectionBaseIsValid = selectionBase > 0 && selectionBase <= ((NSInteger)self.text.length);
+    bool selectionExtentIsValid =
+        selectionExtent > 0 && selectionExtent <= ((NSInteger)self.text.length);
+    if (selectionBaseIsValid && selectionExtentIsValid) {
+      [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:selectedRange]];
+    } else {
+      [self setSelectedTextRangeLocal:[FlutterTextRange rangeWithNSRange:NSMakeRange(0, 0)]];
+    }
+
     _selectionAffinity = _kTextAffinityDownstream;
     if ([state[@"selectionAffinity"] isEqualToString:@(_kTextAffinityUpstream)])
       _selectionAffinity = _kTextAffinityUpstream;
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m
index 2a33bee..b2e6981 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.m
@@ -147,6 +147,26 @@
   OCMReject([engine updateEditingClient:0 withState:[OCMArg any]]);
 }
 
+- (void)testUpdateEditingClientNegativeSelection {
+  FlutterTextInputView* inputView = [[FlutterTextInputView alloc] init];
+  inputView.textInputDelegate = engine;
+
+  [inputView.text setString:@"SELECTION"];
+  inputView.markedTextRange = nil;
+  inputView.selectedTextRange = nil;
+
+  [inputView setTextInputState:@{
+    @"text" : @"SELECTION",
+    @"selectionBase" : @-1,
+    @"selectionExtent" : @-1
+  }];
+  OCMVerify([engine updateEditingClient:0
+                              withState:[OCMArg checkWithBlock:^BOOL(NSDictionary* state) {
+                                return ([state[@"selectionBase"] intValue]) == 0 &&
+                                       ([state[@"selectionExtent"] intValue] == 0);
+                              }]]);
+}
+
 - (void)testAutofillInputViews {
   NSDictionary* template = @{
     @"inputType" : @{@"name" : @"TextInuptType.text"},