Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 5c05519

Browse files
authored
[ios][ios17]fix auto correction highlight on top left corner (#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 #44354 <img width="479" alt="Screenshot 2023-08-16 at 1 19 39 PM" src="https://github.com/flutter/engine/assets/41930132/a5a1dda7-ba21-462e-a65c-1afeecf7559f"> *List which issues are fixed by this PR. You must list at least one issue.* Fixes flutter/flutter#131622 Fixes flutter/flutter#131695 Fixes flutter/flutter#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
1 parent 233a2e0 commit 5c05519

File tree

2 files changed

+102
-6
lines changed

2 files changed

+102
-6
lines changed

shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2449,18 +2449,32 @@ - (void)takeKeyboardScreenshotAndDisplay {
24492449
}
24502450

24512451
- (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2452-
[_activeView setEditableTransform:dictionary[@"transform"]];
2452+
NSArray* transform = dictionary[@"transform"];
2453+
[_activeView setEditableTransform:transform];
2454+
const int leftIndex = 12;
2455+
const int topIndex = 13;
24532456
if ([_activeView isScribbleAvailable]) {
24542457
// This is necessary to set up where the scribble interactable element will be.
2455-
int leftIndex = 12;
2456-
int topIndex = 13;
24572458
_inputHider.frame =
2458-
CGRectMake([dictionary[@"transform"][leftIndex] intValue],
2459-
[dictionary[@"transform"][topIndex] intValue], [dictionary[@"width"] intValue],
2460-
[dictionary[@"height"] intValue]);
2459+
CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2460+
[dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
24612461
_activeView.frame =
24622462
CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
24632463
_activeView.tintColor = [UIColor clearColor];
2464+
} else {
2465+
// TODO(hellohuanlin): Also need to handle iOS 16 case, where the auto-correction highlight does
2466+
// not match the size of text.
2467+
// See https://github.com/flutter/flutter/issues/131695
2468+
if (@available(iOS 17, *)) {
2469+
// Move auto-correction highlight to overlap with the actual text.
2470+
// This is to fix an issue where the system auto-correction highlight is displayed at
2471+
// the top left corner of the screen on iOS 17+.
2472+
// This problem also happens on iOS 16, but the size of highlight does not match the text.
2473+
// See https://github.com/flutter/flutter/issues/131695
2474+
// TODO(hellohuanlin): Investigate if we can use non-zero size.
2475+
_inputHider.frame =
2476+
CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2477+
}
24642478
}
24652479
}
24662480

@@ -2488,7 +2502,22 @@ - (void)setSelectionRects:(NSArray*)encodedRects {
24882502
? NSWritingDirectionLeftToRight
24892503
: NSWritingDirectionRightToLeft]];
24902504
}
2505+
2506+
BOOL shouldNotifyTextChange = NO;
2507+
if (@available(iOS 17, *)) {
2508+
// Force UIKit to query the selectionRects again on iOS 17+
2509+
// This is to fix a bug on iOS 17+ where UIKit queries the outdated selectionRects after
2510+
// entering a character, resulting in auto-correction highlight region missing the last
2511+
// character.
2512+
shouldNotifyTextChange = YES;
2513+
}
2514+
if (shouldNotifyTextChange) {
2515+
[_activeView.inputDelegate textWillChange:_activeView];
2516+
}
24912517
_activeView.selectionRects = rectsAsRect;
2518+
if (shouldNotifyTextChange) {
2519+
[_activeView.inputDelegate textDidChange:_activeView];
2520+
}
24922521
}
24932522

24942523
- (void)startLiveTextInput {

shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ @interface FlutterSecureTextInputView : FlutterTextInputView
6161

6262
@interface FlutterTextInputPlugin ()
6363
@property(nonatomic, assign) FlutterTextInputView* activeView;
64+
@property(nonatomic, readonly) UIView* inputHider;
6465
@property(nonatomic, readonly) UIView* keyboardViewContainer;
6566
@property(nonatomic, readonly) UIView* keyboardView;
6667
@property(nonatomic, assign) UIView* cachedFirstResponder;
@@ -422,6 +423,72 @@ - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
422423
}
423424
}
424425

426+
- (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
427+
FlutterTextInputPlugin* myInputPlugin =
428+
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
429+
430+
FlutterMethodCall* setClientCall =
431+
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
432+
arguments:@[ @(123), self.mutableTemplateCopy ]];
433+
[myInputPlugin handleMethodCall:setClientCall
434+
result:^(id _Nullable result){
435+
}];
436+
437+
FlutterTextInputView* mockInputView = OCMPartialMock(myInputPlugin.activeView);
438+
OCMStub([mockInputView isScribbleAvailable]).andReturn(NO);
439+
440+
// yOffset = 200.
441+
NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
442+
443+
FlutterMethodCall* setPlatformViewClientCall =
444+
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform"
445+
arguments:@{@"transform" : yOffsetMatrix}];
446+
[myInputPlugin handleMethodCall:setPlatformViewClientCall
447+
result:^(id _Nullable result){
448+
}];
449+
450+
if (@available(iOS 17, *)) {
451+
XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
452+
@"The input hider should overlap with the text on and after iOS 17");
453+
454+
} else {
455+
XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
456+
@"The input hider should be on the origin of screen on and before iOS 16.");
457+
}
458+
}
459+
460+
- (void)testSetSelectionRectsNotifiesTextChangeAfterIOS17AndDoesNotNotifyBeforeIOS17 {
461+
FlutterTextInputPlugin* myInputPlugin =
462+
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
463+
464+
FlutterMethodCall* setClientCall =
465+
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
466+
arguments:@[ @(123), self.mutableTemplateCopy ]];
467+
[myInputPlugin handleMethodCall:setClientCall
468+
result:^(id _Nullable result){
469+
}];
470+
471+
id mockInputDelegate = OCMProtocolMock(@protocol(UITextInputDelegate));
472+
myInputPlugin.activeView.inputDelegate = mockInputDelegate;
473+
474+
NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
475+
NSArray* selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
476+
FlutterMethodCall* methodCall =
477+
[FlutterMethodCall methodCallWithMethodName:@"Scribble.setSelectionRects"
478+
arguments:selectionRects];
479+
[myInputPlugin handleMethodCall:methodCall
480+
result:^(id _Nullable result){
481+
}];
482+
483+
if (@available(iOS 17.0, *)) {
484+
OCMVerify([mockInputDelegate textWillChange:myInputPlugin.activeView]);
485+
OCMVerify([mockInputDelegate textDidChange:myInputPlugin.activeView]);
486+
} else {
487+
OCMVerify(never(), [mockInputDelegate textWillChange:myInputPlugin.activeView]);
488+
OCMVerify(never(), [mockInputDelegate textDidChange:myInputPlugin.activeView]);
489+
}
490+
}
491+
425492
- (void)testTextRangeFromPositionMatchesUITextViewBehavior {
426493
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
427494
FlutterTextPosition* fromPosition = [FlutterTextPosition positionWithIndex:2];

0 commit comments

Comments
 (0)