From e3b7e81c8c30840e72b2f44c56a017c8f10b7406 Mon Sep 17 00:00:00 2001 From: Igor Mandrigin Date: Tue, 4 Dec 2018 13:18:08 -0800 Subject: [PATCH] Avoid using `-[UITextView setAttributedString:]` while user is typing (#19809) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: iOS-specific. For languages with complex input (such as Japanese or Chinese), a user has to type multiple characters that are then merged into a single one. If `-[UITextView setAttributedString:]` is used while the user is still typing, it resets the input and characters are not being treated as typed together. This PR avoids calling this method if possible, replacing it by just copying the attributes if the string has not been changed. That preserves the state and user can continue to type Korean or Chinese characters. Fixes #19339 Essentially, the steps to reproduce are described in [the issue](https://github.com/facebook/react-native/issues/19339): 1. Type some Korean characters in TextInput, such as "하늘" (buttons `ㅎ`,`ㅏ`,`ㄴ`,`ㅡ`,`ㄹ`). 2. Then move the cursor to the beginning of the text, type "파란" (buttons `ㅍ`,`ㅏ`,`ㄹ`,`ㅏ`,`ㄴ`) this time. **Behaviour before this fix (broken)** Actual text: `ㅍㅏㄹㅏㄴ하늘`. Expected text: `파란하늘`. Characters aren't combined properly. ![ezgif com-resize](https://user-images.githubusercontent.com/466427/41613572-4256dda8-73f6-11e8-99a9-0ab833202b95.gif) **Behaviour after this fix (correct)** Actual text: `파란하늘`. Expected text: `파란하늘`. Characters are combined, the same behaviour is in vanilla iOS `UITextView`. ![input-with-fix](https://user-images.githubusercontent.com/466427/41613526-1aae2284-73f6-11e8-87f2-c1cef51cd83a.gif) [IOS] [BUGFIX] [TextView] - Fix Korean/Chinese/Japanese input for multiline TextView on iOS. Pull Request resolved: https://github.com/facebook/react-native/pull/19809 Differential Revision: D13326614 Pulled By: shergin fbshipit-source-id: 6a5cab3f7290f0f623a6f4c29353a573eb321b0b --- .../Text/TextInput/Multiline/RCTUITextView.m | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Libraries/Text/TextInput/Multiline/RCTUITextView.m b/Libraries/Text/TextInput/Multiline/RCTUITextView.m index ca206e39424683..c50638fc54aa3c 100644 --- a/Libraries/Text/TextInput/Multiline/RCTUITextView.m +++ b/Libraries/Text/TextInput/Multiline/RCTUITextView.m @@ -110,7 +110,21 @@ - (void)setTextAlignment:(NSTextAlignment)textAlignment - (void)setAttributedText:(NSAttributedString *)attributedText { - [super setAttributedText:attributedText]; + // Using `setAttributedString:` while user is typing breaks some internal mechanics + // when entering complex input languages such as Chinese, Korean or Japanese. + // see: https://github.com/facebook/react-native/issues/19339 + + // We try to avoid calling this method as much as we can. + // If the text has changed, there is nothing we can do. + if (![super.attributedText.string isEqualToString:attributedText.string]) { + [super setAttributedText:attributedText]; + } else { + // But if the text is preserved, we just copying the attributes from the source string. + if (![super.attributedText isEqualToAttributedString:attributedText]) { + [self copyTextAttributesFrom:attributedText]; + } + } + [self textDidChange]; } @@ -241,4 +255,20 @@ - (void)invalidatePlaceholderVisibility _placeholderView.hidden = !isVisible; } +#pragma mark - Utility Methods + +- (void)copyTextAttributesFrom:(NSAttributedString *)sourceString +{ + [self.textStorage beginEditing]; + + NSTextStorage *textStorage = self.textStorage; + [sourceString enumerateAttributesInRange:NSMakeRange(0, sourceString.length) + options:NSAttributedStringEnumerationReverse + usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { + [textStorage setAttributes:attrs range:range]; + }]; + + [self.textStorage endEditing]; +} + @end