From b5856e7af49209b6b312e8844651a3c5139d7cee Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 20 Sep 2022 13:03:24 -0700 Subject: [PATCH 1/2] draft fix format fix fix fix delete multiple remove unused code add comments format fix comments fix clang tidy fix autorelease tests release temporarilyDeletedComposedCharacter after use, use more accurate verbose --- shell/platform/darwin/ios/BUILD.gn | 1 + .../Source/FlutterTextInputPlugin.mm | 28 +++++++++ .../Source/FlutterTextInputPluginTest.mm | 59 +++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 27fc1f7f4e196..6996df7248933 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -174,6 +174,7 @@ source_set("flutter_framework_source") { frameworks = [ "AudioToolbox.framework", "CoreMedia.framework", + "CoreText.framework", "CoreVideo.framework", "QuartzCore.framework", "UIKit.framework", diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index edb7d70a33d71..f2335396ab67a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -4,6 +4,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" +#import #import #import @@ -702,6 +703,10 @@ @interface FlutterTextInputView () @property(nonatomic, assign) CGRect markedRect; @property(nonatomic) BOOL isVisibleToAutofill; @property(nonatomic, assign) BOOL accessibilityEnabled; +// The composed character that is temporarily removed by the keyboard API. +// This is cleared at the start of each keyboard interaction. (Enter a character, delete a character +// etc) +@property(nonatomic, copy) NSString* temporarilyDeletedComposedCharacter; - (void)setEditableTransform:(NSArray*)matrix; @end @@ -880,6 +885,8 @@ - (void)dealloc { [_markedTextStyle release]; [_textContentType release]; [_textInteraction release]; + [_temporarilyDeletedComposedCharacter release]; + _temporarilyDeletedComposedCharacter = nil; [super dealloc]; } @@ -1224,6 +1231,10 @@ - (void)replaceRange:(UITextRange*)range withText:(NSString*)text { } - (BOOL)shouldChangeTextInRange:(UITextRange*)range replacementText:(NSString*)text { + // `temporarilyDeletedComposedCharacter` should only be used during a single text change session. + // So it needs to be cleared at the start of each text editting session. + self.temporarilyDeletedComposedCharacter = nil; + if (self.returnKeyType == UIReturnKeyDefault && [text isEqualToString:@"\n"]) { [self.textInputDelegate flutterTextInputView:self performAction:FlutterTextInputActionNewline @@ -1848,6 +1859,15 @@ - (BOOL)hasText { } - (void)insertText:(NSString*)text { + if (self.temporarilyDeletedComposedCharacter.length > 0 && text.length == 1 && !text.UTF8String && + [text characterAtIndex:0] == [self.temporarilyDeletedComposedCharacter characterAtIndex:0]) { + // Workaround for https://github.com/flutter/flutter/issues/111494 + // TODO(cyanglaz): revert this workaround if when flutter supports a minimum iOS version which + // this bug is fixed by Apple. + text = self.temporarilyDeletedComposedCharacter; + self.temporarilyDeletedComposedCharacter = nil; + } + NSMutableArray* copiedRects = [[NSMutableArray alloc] initWithCapacity:[_selectionRects count]]; NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]], @@ -1936,6 +1956,14 @@ - (void)deleteBackward { } if (!_selectedTextRange.isEmpty) { + // Cache the last deleted composed characters to use for an iOS bug where the next + // insertion corrupts the composed characters. + // See: https://github.com/flutter/flutter/issues/111494#issuecomment-1248441346 + NSString* deletedText = [self.text substringWithRange:_selectedTextRange.range]; + CFRange range = + CFStringGetRangeOfComposedCharactersAtIndex((__bridge CFStringRef)deletedText, 0); + self.temporarilyDeletedComposedCharacter = + [deletedText substringWithRange:NSMakeRange(range.location, range.length)]; [self replaceRange:_selectedTextRange withText:@""]; } } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index 09c307ef0dba8..2e0198bafec3a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -453,6 +453,65 @@ - (void)testDeletingBackward { XCTAssertEqualObjects(inputView.text, @""); } +// This tests the workaround to fix an iOS 16 bug +// See: https://github.com/flutter/flutter/issues/111494 +- (void)testSystemOnlyAddingPartialComposedCharacter { + NSDictionary* config = self.mutableTemplateCopy; + [self setClientId:123 configuration:config]; + NSArray* inputFields = self.installedInputViews; + FlutterTextInputView* inputView = inputFields[0]; + + [inputView insertText:@"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"]; + [inputView deleteBackward]; + + // Insert the first unichar in the emoji. + [inputView insertText:[@"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ" substringWithRange:NSMakeRange(0, 1)]]; + [inputView insertText:@"์•„"]; + + XCTAssertEqualObjects(inputView.text, @"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ์•„"); + + // Deleting ์•„. + [inputView deleteBackward]; + // ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ should be the current string. + + [inputView insertText:@"๐Ÿ˜€"]; + [inputView deleteBackward]; + // Insert the first unichar in the emoji. + [inputView insertText:[@"๐Ÿ˜€" substringWithRange:NSMakeRange(0, 1)]]; + [inputView insertText:@"์•„"]; + XCTAssertEqualObjects(inputView.text, @"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ˜€์•„"); + + // Deleting ์•„. + [inputView deleteBackward]; + // ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ˜€ should be the current string. + + [inputView deleteBackward]; + // Insert the first unichar in the emoji. + [inputView insertText:[@"๐Ÿ˜€" substringWithRange:NSMakeRange(0, 1)]]; + [inputView insertText:@"์•„"]; + + XCTAssertEqualObjects(inputView.text, @"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ๐Ÿ˜€์•„"); +} + +- (void)testCachedComposedCharacterClearedAtKeyboardInteraction { + NSDictionary* config = self.mutableTemplateCopy; + [self setClientId:123 configuration:config]; + NSArray* inputFields = self.installedInputViews; + FlutterTextInputView* inputView = inputFields[0]; + + [inputView insertText:@"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"]; + [inputView deleteBackward]; + [inputView shouldChangeTextInRange:OCMClassMock([UITextRange class]) replacementText:@""]; + + // Insert the first unichar in the emoji. + NSString* brokenEmoji = [@"๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ" substringWithRange:NSMakeRange(0, 1)]; + [inputView insertText:brokenEmoji]; + [inputView insertText:@"์•„"]; + + NSString* finalText = [NSString stringWithFormat:@"%@์•„", brokenEmoji]; + XCTAssertEqualObjects(inputView.text, finalText); +} + - (void)testPastingNonTextDisallowed { NSDictionary* config = self.mutableTemplateCopy; [self setClientId:123 configuration:config]; From 9afb4e17ff3c9868c0bbbbf3174a88e6434337d6 Mon Sep 17 00:00:00 2001 From: Chris Yang Date: Tue, 4 Oct 2022 12:52:55 -0700 Subject: [PATCH 2/2] review --- shell/platform/darwin/ios/BUILD.gn | 1 - .../Source/FlutterTextInputPlugin.mm | 39 +++++++++++-------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 6996df7248933..27fc1f7f4e196 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -174,7 +174,6 @@ source_set("flutter_framework_source") { frameworks = [ "AudioToolbox.framework", "CoreMedia.framework", - "CoreText.framework", "CoreVideo.framework", "QuartzCore.framework", "UIKit.framework", diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index f2335396ab67a..70ac5d1d784e7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -4,7 +4,6 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" -#import #import #import @@ -75,6 +74,19 @@ #pragma mark - Static Functions +// Determine if the character at `range` of `text` is an emoji. +static BOOL IsEmoji(NSString* text, NSRange charRange) { + UChar32 codePoint; + BOOL gotCodePoint = [text getBytes:&codePoint + maxLength:sizeof(codePoint) + usedLength:NULL + encoding:NSUTF32StringEncoding + options:kNilOptions + range:charRange + remainingRange:NULL]; + return gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI); +} + // "TextInputType.none" is a made-up input type that's typically // used when there's an in-app virtual keyboard. If // "TextInputType.none" is specified, disable the system @@ -1938,15 +1950,7 @@ - (void)deleteBackward { // We should check if the last character is a part of emoji. // If so, we must delete the entire emoji to prevent the text from being malformed. NSRange charRange = fml::RangeForCharacterAtIndex(self.text, oldRange.location - 1); - UChar32 codePoint; - BOOL gotCodePoint = [self.text getBytes:&codePoint - maxLength:sizeof(codePoint) - usedLength:NULL - encoding:NSUTF32StringEncoding - options:kNilOptions - range:charRange - remainingRange:NULL]; - if (gotCodePoint && u_hasBinaryProperty(codePoint, UCHAR_EMOJI)) { + if (IsEmoji(self.text, charRange)) { newRange = NSMakeRange(charRange.location, oldRange.location - charRange.location); } @@ -1956,14 +1960,15 @@ - (void)deleteBackward { } if (!_selectedTextRange.isEmpty) { - // Cache the last deleted composed characters to use for an iOS bug where the next - // insertion corrupts the composed characters. + // Cache the last deleted emoji to use for an iOS bug where the next + // insertion corrupts the emoji characters. // See: https://github.com/flutter/flutter/issues/111494#issuecomment-1248441346 - NSString* deletedText = [self.text substringWithRange:_selectedTextRange.range]; - CFRange range = - CFStringGetRangeOfComposedCharactersAtIndex((__bridge CFStringRef)deletedText, 0); - self.temporarilyDeletedComposedCharacter = - [deletedText substringWithRange:NSMakeRange(range.location, range.length)]; + if (IsEmoji(self.text, _selectedTextRange.range)) { + NSString* deletedText = [self.text substringWithRange:_selectedTextRange.range]; + NSRange deleteFirstCharacterRange = fml::RangeForCharacterAtIndex(deletedText, 0); + self.temporarilyDeletedComposedCharacter = + [deletedText substringWithRange:deleteFirstCharacterRange]; + } [self replaceRange:_selectedTextRange withText:@""]; } }