From 380e7d740261ddfdfc701ce0e6c1b9c52ea1fcde Mon Sep 17 00:00:00 2001
From: hellohuanlin <41930132+hellohuanlin@users.noreply.github.com>
Date: Thu, 17 Aug 2023 09:48:06 -0700
Subject: [PATCH] [ios][ios17]fix auto correction highlight on top left corner
(flutter/engine#44779)
Fix native auto-correction highlight region on top left corner.
This PR uses the system auto-correction highlight on iOS 17, which was disabled by https://github.com/flutter/engine/pull/44354
*List which issues are fixed by this PR. You must list at least one issue.*
Fixes https://github.com/flutter/flutter/issues/131622
Fixes https://github.com/flutter/flutter/issues/131695
Fixes https://github.com/flutter/flutter/issues/130818
*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].*
[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
---
.../Source/FlutterTextInputPlugin.mm | 41 ++++++++++--
.../Source/FlutterTextInputPluginTest.mm | 67 +++++++++++++++++++
2 files changed, 102 insertions(+), 6 deletions(-)
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
index 0d6ecbe9b6..d8fc121a5d 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
@@ -2449,18 +2449,32 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point,
}
- (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);
+ }
}
}
@@ -2488,7 +2502,22 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point,
? 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/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
index 5a214281eb..b6b1967ceb 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
@@ -61,6 +61,7 @@ FLUTTER_ASSERT_ARC
@interface FlutterTextInputPlugin ()
@property(nonatomic, assign) FlutterTextInputView* activeView;
+@property(nonatomic, readonly) UIView* inputHider;
@property(nonatomic, readonly) UIView* keyboardViewContainer;
@property(nonatomic, readonly) UIView* keyboardView;
@property(nonatomic, assign) UIView* cachedFirstResponder;
@@ -422,6 +423,72 @@ FLUTTER_ASSERT_ARC
}
}
+- (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];