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

Commit ba351d1

Browse files
authored
[cp][ios][ios17]fix auto correction highlight on top left corner on iOS 17 (again) (#44812)
CP for #44779 Tested the stable branch with the fix on iPhone and iPad, with hardware and software keyboards, and with and without IME languages, iOS 16 and iOS 17 *Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* *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 1ac611c commit ba351d1

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
@@ -2289,18 +2289,32 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
22892289
}
22902290

22912291
- (void)setEditableSizeAndTransform:(NSDictionary*)dictionary {
2292-
[_activeView setEditableTransform:dictionary[@"transform"]];
2292+
NSArray* transform = dictionary[@"transform"];
2293+
[_activeView setEditableTransform:transform];
2294+
const int leftIndex = 12;
2295+
const int topIndex = 13;
22932296
if ([_activeView isScribbleAvailable]) {
22942297
// This is necessary to set up where the scribble interactable element will be.
2295-
int leftIndex = 12;
2296-
int topIndex = 13;
22972298
_inputHider.frame =
2298-
CGRectMake([dictionary[@"transform"][leftIndex] intValue],
2299-
[dictionary[@"transform"][topIndex] intValue], [dictionary[@"width"] intValue],
2300-
[dictionary[@"height"] intValue]);
2299+
CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue],
2300+
[dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
23012301
_activeView.frame =
23022302
CGRectMake(0, 0, [dictionary[@"width"] intValue], [dictionary[@"height"] intValue]);
23032303
_activeView.tintColor = [UIColor clearColor];
2304+
} else {
2305+
// TODO(hellohuanlin): Also need to handle iOS 16 case, where the auto-correction highlight does
2306+
// not match the size of text.
2307+
// See https://github.com/flutter/flutter/issues/131695
2308+
if (@available(iOS 17, *)) {
2309+
// Move auto-correction highlight to overlap with the actual text.
2310+
// This is to fix an issue where the system auto-correction highlight is displayed at
2311+
// the top left corner of the screen on iOS 17+.
2312+
// This problem also happens on iOS 16, but the size of highlight does not match the text.
2313+
// See https://github.com/flutter/flutter/issues/131695
2314+
// TODO(hellohuanlin): Investigate if we can use non-zero size.
2315+
_inputHider.frame =
2316+
CGRectMake([transform[leftIndex] intValue], [transform[topIndex] intValue], 0, 0);
2317+
}
23042318
}
23052319
}
23062320

@@ -2328,7 +2342,22 @@ - (void)setSelectionRects:(NSArray*)encodedRects {
23282342
? NSWritingDirectionLeftToRight
23292343
: NSWritingDirectionRightToLeft]];
23302344
}
2345+
2346+
BOOL shouldNotifyTextChange = NO;
2347+
if (@available(iOS 17, *)) {
2348+
// Force UIKit to query the selectionRects again on iOS 17+
2349+
// This is to fix a bug on iOS 17+ where UIKit queries the outdated selectionRects after
2350+
// entering a character, resulting in auto-correction highlight region missing the last
2351+
// character.
2352+
shouldNotifyTextChange = YES;
2353+
}
2354+
if (shouldNotifyTextChange) {
2355+
[_activeView.inputDelegate textWillChange:_activeView];
2356+
}
23312357
_activeView.selectionRects = rectsAsRect;
2358+
if (shouldNotifyTextChange) {
2359+
[_activeView.inputDelegate textDidChange:_activeView];
2360+
}
23322361
}
23332362

23342363
- (void)startLiveTextInput {

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

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

6161
@interface FlutterTextInputPlugin ()
6262
@property(nonatomic, assign) FlutterTextInputView* activeView;
63+
@property(nonatomic, readonly) UIView* inputHider;
6364
@property(nonatomic, readonly)
6465
NSMutableDictionary<NSString*, FlutterTextInputView*>* autofillContext;
6566

@@ -401,6 +402,72 @@ - (void)testAutocorrectionPromptRectDoesNotAppearDuringScribble {
401402
}
402403
}
403404

405+
- (void)testInputHiderOverlapWithTextWhenScribbleIsDisabledAfterIOS17AndDoesNotOverlapBeforeIOS17 {
406+
FlutterTextInputPlugin* myInputPlugin =
407+
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
408+
409+
FlutterMethodCall* setClientCall =
410+
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
411+
arguments:@[ @(123), self.mutableTemplateCopy ]];
412+
[myInputPlugin handleMethodCall:setClientCall
413+
result:^(id _Nullable result){
414+
}];
415+
416+
FlutterTextInputView* mockInputView = OCMPartialMock(myInputPlugin.activeView);
417+
OCMStub([mockInputView isScribbleAvailable]).andReturn(NO);
418+
419+
// yOffset = 200.
420+
NSArray* yOffsetMatrix = @[ @1, @0, @0, @0, @0, @1, @0, @0, @0, @0, @1, @0, @0, @200, @0, @1 ];
421+
422+
FlutterMethodCall* setPlatformViewClientCall =
423+
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setEditableSizeAndTransform"
424+
arguments:@{@"transform" : yOffsetMatrix}];
425+
[myInputPlugin handleMethodCall:setPlatformViewClientCall
426+
result:^(id _Nullable result){
427+
}];
428+
429+
if (@available(iOS 17, *)) {
430+
XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectMake(0, 200, 0, 0)),
431+
@"The input hider should overlap with the text on and after iOS 17");
432+
433+
} else {
434+
XCTAssert(CGRectEqualToRect(myInputPlugin.inputHider.frame, CGRectZero),
435+
@"The input hider should be on the origin of screen on and before iOS 16.");
436+
}
437+
}
438+
439+
- (void)testSetSelectionRectsNotifiesTextChangeAfterIOS17AndDoesNotNotifyBeforeIOS17 {
440+
FlutterTextInputPlugin* myInputPlugin =
441+
[[FlutterTextInputPlugin alloc] initWithDelegate:OCMClassMock([FlutterEngine class])];
442+
443+
FlutterMethodCall* setClientCall =
444+
[FlutterMethodCall methodCallWithMethodName:@"TextInput.setClient"
445+
arguments:@[ @(123), self.mutableTemplateCopy ]];
446+
[myInputPlugin handleMethodCall:setClientCall
447+
result:^(id _Nullable result){
448+
}];
449+
450+
id mockInputDelegate = OCMProtocolMock(@protocol(UITextInputDelegate));
451+
myInputPlugin.activeView.inputDelegate = mockInputDelegate;
452+
453+
NSArray<NSNumber*>* selectionRect = [NSArray arrayWithObjects:@0, @0, @100, @100, @0, @1, nil];
454+
NSArray* selectionRects = [NSArray arrayWithObjects:selectionRect, nil];
455+
FlutterMethodCall* methodCall =
456+
[FlutterMethodCall methodCallWithMethodName:@"Scribble.setSelectionRects"
457+
arguments:selectionRects];
458+
[myInputPlugin handleMethodCall:methodCall
459+
result:^(id _Nullable result){
460+
}];
461+
462+
if (@available(iOS 17.0, *)) {
463+
OCMVerify([mockInputDelegate textWillChange:myInputPlugin.activeView]);
464+
OCMVerify([mockInputDelegate textDidChange:myInputPlugin.activeView]);
465+
} else {
466+
OCMVerify(never(), [mockInputDelegate textWillChange:myInputPlugin.activeView]);
467+
OCMVerify(never(), [mockInputDelegate textDidChange:myInputPlugin.activeView]);
468+
}
469+
}
470+
404471
- (void)testTextRangeFromPositionMatchesUITextViewBehavior {
405472
FlutterTextInputView* inputView = [[FlutterTextInputView alloc] initWithOwner:textInputPlugin];
406473
FlutterTextPosition* fromPosition = [FlutterTextPosition positionWithIndex:2];

0 commit comments

Comments
 (0)