diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index f02ab85da7ece..09946dc0e50e1 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -602,7 +602,7 @@ - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position case UITextGranularityLine: // The default UITextInputStringTokenizer does not handle line granularity // correctly. We need to implement our own line tokenizer. - result = [self lineEnclosingPosition:position]; + result = [self lineEnclosingPosition:position inDirection:direction]; break; case UITextGranularityCharacter: case UITextGranularityWord: @@ -618,7 +618,21 @@ - (UITextRange*)rangeEnclosingPosition:(UITextPosition*)position return result; } -- (UITextRange*)lineEnclosingPosition:(UITextPosition*)position { +- (UITextRange*)lineEnclosingPosition:(UITextPosition*)position + inDirection:(UITextDirection)direction { + // TODO(hellohuanlin): remove iOS 17 check. The same logic should apply to older iOS version. + if (@available(iOS 17.0, *)) { + // According to the API doc if the text position is at a text-unit boundary, it is considered + // enclosed only if the next position in the given direction is entirely enclosed. Link: + // https://developer.apple.com/documentation/uikit/uitextinputtokenizer/1614464-rangeenclosingposition?language=objc + FlutterTextPosition* flutterPosition = (FlutterTextPosition*)position; + if (flutterPosition.index > _textInputView.text.length || + (flutterPosition.index == _textInputView.text.length && + direction == UITextStorageDirectionForward)) { + return nil; + } + } + // Gets the first line break position after the input position. NSString* textAfter = [_textInputView textInRange:[_textInputView textRangeFromPosition:position diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index f18357acf0ffc..8a6cc6b53a305 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -2660,6 +2660,54 @@ - (void)testFlutterTokenizerCanParseLines { XCTAssertEqual(range.range.length, 20u); } +- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInBackwardDirectionShouldNotReturnNil { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; + [inputView insertText:@"0123456789\n012345"]; + id tokenizer = [inputView tokenizer]; + + FlutterTextRange* range = + (FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument] + withGranularity:UITextGranularityLine + inDirection:UITextStorageDirectionBackward]; + XCTAssertEqual(range.range.location, 11u); + XCTAssertEqual(range.range.length, 6u); +} + +- (void)testFlutterTokenizerLineEnclosingEndOfDocumentInForwardDirectionShouldReturnNilOnIOS17 { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; + [inputView insertText:@"0123456789\n012345"]; + id tokenizer = [inputView tokenizer]; + + FlutterTextRange* range = + (FlutterTextRange*)[tokenizer rangeEnclosingPosition:[inputView endOfDocument] + withGranularity:UITextGranularityLine + inDirection:UITextStorageDirectionForward]; + if (@available(iOS 17.0, *)) { + XCTAssertNil(range); + } else { + XCTAssertEqual(range.range.location, 11u); + XCTAssertEqual(range.range.length, 6u); + } +} + +- (void)testFlutterTokenizerLineEnclosingOutOfRangePositionShouldReturnNilOnIOS17 { + FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; + [inputView insertText:@"0123456789\n012345"]; + id tokenizer = [inputView tokenizer]; + + FlutterTextPosition* position = [FlutterTextPosition positionWithIndex:100]; + FlutterTextRange* range = + (FlutterTextRange*)[tokenizer rangeEnclosingPosition:position + withGranularity:UITextGranularityLine + inDirection:UITextStorageDirectionForward]; + if (@available(iOS 17.0, *)) { + XCTAssertNil(range); + } else { + XCTAssertEqual(range.range.location, 0u); + XCTAssertEqual(range.range.length, 0u); + } +} + - (void)testFlutterTextInputPluginRetainsFlutterTextInputView { FlutterViewController* flutterViewController = [[FlutterViewController alloc] init]; FlutterTextInputPlugin* myInputPlugin = [[FlutterTextInputPlugin alloc] initWithDelegate:engine];