From 96ee5d99d54f43b6c88378dd630d6992eb3f5f47 Mon Sep 17 00:00:00 2001 From: Huan Lin Date: Wed, 16 Aug 2023 13:09:26 -0700 Subject: [PATCH] [ios][ios17]fix auto correction highlight on top left corner address some comments and nits fix todo updated some comments --- .../Source/FlutterTextInputPlugin.mm | 41 ++++++++++-- .../Source/FlutterTextInputPluginTest.mm | 67 +++++++++++++++++++ 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index 9072d02f2f785..78ddc45602375 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -2289,18 +2289,32 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } - (void)setEditableSizeAndTransform:(NSDictionary*)dictionary { - [_activeView setEditableTransform:dictionary[@"transform"]]; + NSArray* transform = dictionary[@"transform"]; + [_activeView setEditableTransform:transform]; + const int leftIndex = 12; + const int topIndex = 13; if ([_activeView isScribbleAvailable]) { // This is necessary to set up where the scribble interactable element will be. - int leftIndex = 12; - int topIndex = 13; _inputHider.frame = - CGRectMake([dictionary[@"transform"][leftIndex] intValue], - [dictionary[@"transform"][topIndex] intValue], [dictionary[@"width"] intValue], - [dictionary[@"height"] intValue]); + CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], + [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]); _activeView.frame = CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]); _activeView.tintColor = [UIColor clearColor]; + } else { + // TODO(hellohuanlin): Also need to handle iOS 16 case, where the auto-correction highlight does + // not match the size of text. + // See https://github.com/flutter/flutter/issues/131695 + if (@available(iOS 17, *)) { + // Move auto-correction highlight to overlap with the actual text. + // This is to fix an issue where the system auto-correction highlight is displayed at + // the top left corner of the screen on iOS 17+. + // This problem also happens on iOS 16, but the size of highlight does not match the text. + // See https://github.com/flutter/flutter/issues/131695 + // TODO(hellohuanlin): Investigate if we can use non-zero size. + _inputHider.frame = + CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0); + } } } @@ -2328,7 +2342,22 @@ - (void)setSelectionRects:(NSArray*)encodedRects { ? NSWritingDirectionLeftToRight : NSWritingDirectionRightToLeft]]; } + + BOOL shouldNotifyTextChange = NO; + if (@available(iOS 17, *)) { + // Force UIKit to query the selectionRects again on iOS 17+ + // This is to fix a bug on iOS 17+ where UIKit queries the outdated selectionRects after + // entering a character, resulting in auto-correction highlight region missing the last + // character. + shouldNotifyTextChange = YES; + } + if (shouldNotifyTextChange) { + [_activeView.inputDelegate textWillChange:_activeView]; + } _activeView.selectionRects = rectsAsRect; + if (shouldNotifyTextChange) { + [_activeView.inputDelegate textDidChange:_activeView]; + } } - (void)startLiveTextInput { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index c39fa27ae6fbb..1019e35213723 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -60,6 +60,7 @@ @interface FlutterSecureTextInputView : FlutterTextInputView @interface FlutterTextInputPlugin () @property(nonatomic, assign) FlutterTextInputView* activeView; +@property(nonatomic, readonly) UIView* inputHider; @property(nonatomic, readonly) NSMutableDictionary* autofillContext; @@ -401,6 +402,72 @@ - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble { } } +- (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 { + FlutterTextInputPlugin* myInputPlugin = + [[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])]; + + FlutterMethodCall* setClientCall = + [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ @(123), self.mutableTemplateCopy ]]; + [myInputPlugin handleMethodCall:setClientCall + result:^(id _Nullable result){ + }]; + + FlutterTextInputView* mockInputView = OCMPartialMock(myInputPlugin.activeView); + OCMStub([mockInputView isScribbleAvailable]).andReturn(NO); + + // yOffset = 200. + NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ]; + + FlutterMethodCall* setPlatformViewClientCall = + [FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform" + arguments:@{@"transform" : yOffsetMatrix}]; + [myInputPlugin handleMethodCall:setPlatformViewClientCall + result:^(id _Nullable result){ + }]; + + if (@available(iOS 17, *)) { + XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)), + @"The input hider should overlap with the text on and after iOS 17"); + + } else { + XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero), + @"The input hider should be on the origin of screen on and before iOS 16."); + } +} + +- (void)testSetSelectionRectsNotifiesTextChangeAfterIOS17AndDoesNotNotifyBeforeIOS17 { + FlutterTextInputPlugin* myInputPlugin = + [[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])]; + + FlutterMethodCall* setClientCall = + [FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient" + arguments:@[ @(123), self.mutableTemplateCopy ]]; + [myInputPlugin handleMethodCall:setClientCall + result:^(id _Nullable result){ + }]; + + id mockInputDelegate = OCMProtocolMock(@protocol(UITextInputDelegate)); + myInputPlugin.activeView.inputDelegate = mockInputDelegate; + + NSArray* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil]; + NSArray* selectionRects = [NSArray arrayWithObjects:selectionRect, nil]; + FlutterMethodCall* methodCall = + [FlutterMethodCall methodCallWithMethodName:@"Scribble.setSelectionRects" + arguments:selectionRects]; + [myInputPlugin handleMethodCall:methodCall + result:^(id _Nullable result){ + }]; + + if (@available(iOS 17.0, *)) { + OCMVerify([mockInputDelegate textWillChange:myInputPlugin.activeView]); + OCMVerify([mockInputDelegate textDidChange:myInputPlugin.activeView]); + } else { + OCMVerify(never(), [mockInputDelegate textWillChange:myInputPlugin.activeView]); + OCMVerify(never(), [mockInputDelegate textDidChange:myInputPlugin.activeView]); + } +} + - (void)testTextRangeFromPositionMatchesUITextViewBehavior { FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin]; FlutterTextPosition* fromPosition = [FlutterTextPosition positionWithIndex:2];