Skip to content

Commit

Permalink
Workaround iOS text input crash for emoji+Korean text (#36295)
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Yang committed Oct 17, 2022
1 parent db5d3ef commit c92f1ca
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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
Expand Down Expand Up @@ -715,6 +728,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
Expand Down Expand Up @@ -901,6 +918,8 @@ - (void)dealloc {
[_markedTextStyle release];
[_textContentType release];
[_textInteraction release];
[_temporarilyDeletedComposedCharacter release];
_temporarilyDeletedComposedCharacter = nil;
[super dealloc];
}

Expand Down Expand Up @@ -1245,6 +1264,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
Expand Down Expand Up @@ -1832,6 +1855,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<FlutterTextSelectionRect*>* copiedRects =
[[NSMutableArray alloc] initWithCapacity:[_selectionRects count]];
NSAssert([_selectedTextRange.start isKindOfClass:[FlutterTextPosition class]],
Expand Down Expand Up @@ -1902,15 +1934,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);
}

Expand All @@ -1920,6 +1944,15 @@ - (void)deleteBackward {
}

if (!_selectedTextRange.isEmpty) {
// 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
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:@""];
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,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<FlutterTextInputView*>* 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<FlutterTextInputView*>* 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];
Expand Down

0 comments on commit c92f1ca

Please sign in to comment.