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

Commit 7d09940

Browse files
committed
[ios][ios17]fix auto correction highlight on top left corner
1 parent dbfe71c commit 7d09940

File tree

2 files changed

+94
-7
lines changed

2 files changed

+94
-7
lines changed

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

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

24512451
- (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2452-
[_activeView setEditableTransform:dictionary[@"transform"]];
2452+
NSArray* transform = dictionary[@"transform"];
2453+
[_activeView setEditableTransform:transform];
2454+
int leftIndex = 12;
2455+
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+
if (@available(iOS 17, *)) {
2466+
// Move auto-correction highlight to overlap with the actual text.
2467+
// This is to fix an issue where the system auto-correction highlight is displayed at
2468+
// the top left corner of the screen on iOS 17+.
2469+
// See https://github.com/flutter/flutter/issues/131695
2470+
_inputHider.frame =
2471+
CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2472+
}
24642473
}
24652474
}
24662475

@@ -2488,7 +2497,18 @@ - (void)setSelectionRects:(NSArray*)encodedRects {
24882497
? NSWritingDirectionLeftToRight
24892498
: NSWritingDirectionRightToLeft]];
24902499
}
2491-
_activeView.selectionRects = rectsAsRect;
2500+
2501+
if (@available(iOS 17, *)) {
2502+
// Force UIKit to query the selectionRects again on iOS 17+
2503+
// This is to fix a bug on iOS 17+ where UIKit queries the outdated selectionRects after
2504+
// entering a character, resulting in auto-correction highlight region missing the last
2505+
// character.
2506+
[_activeView.inputDelegate textWillChange:_activeView];
2507+
_activeView.selectionRects = rectsAsRect;
2508+
[_activeView.inputDelegate textDidChange:_activeView];
2509+
} else {
2510+
_activeView.selectionRects = rectsAsRect;
2511+
}
24922512
}
24932513

24942514
- (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)