├── README.md ├── package.json ├── patch.js └── patches ├── RCTUITextField.m └── RCTUITextView.m /README.md: -------------------------------------------------------------------------------- 1 | [![NPM Version][npm-image]][npm-url] 2 | 3 | TextInput is shaking when inputing with CJK. 4 | 5 | **This problem has been fixed in RN v0.63.1, But if you can't use this version, this patch will fix the problem for you!** 6 | 7 | # Usage 8 | 9 | ### Install 10 | Once installed, react-native's TextInput is automatically patched. 11 | ```bash 12 | yarn add react-native-cjk-textinput-patch --dev 13 | ``` 14 | 15 | `postinstall`, `postuninstall` should be added to prevent this patch from being restored whenever packages are changed. 16 | ```javascript 17 | { 18 | ... 19 | "scripts": { 20 | ..., 21 | "postinstall": "yarn run cjk-textinput-patch", 22 | "postuninstall": "yarn run cjk-textinput-patch" 23 | } 24 | } 25 | ``` 26 | 27 | If you were already using `postinstall`, you can add the patch script later. 28 | ```javascript 29 | "postinstall": "yarn run jetify; yarn run cjk-textinput-patch" 30 | ``` 31 | 32 | ### Execute manually 33 | You can execute the patch manually with the command below. 34 | ```bash 35 | yarn run cjk-textinput-patch 36 | ``` 37 | 38 | ### Uninstall 39 | Just delete the command you added to `postinstall`, `postuninstall` and remove my package. 40 | 41 | [npm-image]: https://img.shields.io/npm/v/react-native-cjk-textinput-patch.svg?style=flat-square 42 | [npm-url]: https://npmjs.org/package/react-native-cjk-textinput-patch 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-cjk-textinput-patch", 3 | "version": "1.0.0", 4 | "description": "Patch for TextInput problem in CJK", 5 | "keywords": [ 6 | "react-native", 7 | "textinput", 8 | "cjk" 9 | ], 10 | "main": "patch.js", 11 | "author": "ifsnow ", 12 | "license": "MIT", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/ifsnow/react-native-cjk-textinput-patch.git" 16 | }, 17 | "bin": { 18 | "cjk-textinput-patch": "patch.js" 19 | }, 20 | "scripts": { 21 | "patch": "node patch.js", 22 | "postinstall": "yarn run patch" 23 | }, 24 | "devDependencies": { 25 | "react-native": ">=0.60.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /patch.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const rootPath = path.join(__dirname, '.'); 7 | 8 | const packageFile = fs.readFileSync(`${rootPath}/../react-native/package.json`); 9 | const package = JSON.parse(packageFile); 10 | 11 | const version = package.version.split('.'); 12 | const majorVersion = parseInt(version[0], 10); 13 | const minorVersion = parseInt(version[1], 10); 14 | const patchVersion = parseInt(version[2], 10); 15 | 16 | if (minorVersion !== 62) { 17 | console.log('[!] This patch is for React Native 0.62.x!'); 18 | process.exit(1); 19 | } 20 | 21 | fs.copyFileSync( 22 | `${rootPath}/patches/RCTUITextView.m`, 23 | `${rootPath}/../react-native/Libraries/Text/TextInput/Multiline/RCTUITextView.m`, 24 | ); 25 | 26 | fs.copyFileSync( 27 | `${rootPath}/patches/RCTUITextField.m`, 28 | `${rootPath}/../react-native/Libraries/Text/TextInput/Singleline/RCTUITextField.m`, 29 | ); 30 | 31 | console.log('[!] TextInput was patched!'); 32 | -------------------------------------------------------------------------------- /patches/RCTUITextField.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import 11 | #import 12 | 13 | #import 14 | #import 15 | 16 | @implementation RCTUITextField { 17 | RCTBackedTextFieldDelegateAdapter *_textInputDelegateAdapter; 18 | NSDictionary *_defaultTextAttributes; 19 | } 20 | 21 | - (instancetype)initWithFrame:(CGRect)frame 22 | { 23 | if (self = [super initWithFrame:frame]) { 24 | [[NSNotificationCenter defaultCenter] addObserver:self 25 | selector:@selector(_textDidChange) 26 | name:UITextFieldTextDidChangeNotification 27 | object:self]; 28 | 29 | _textInputDelegateAdapter = [[RCTBackedTextFieldDelegateAdapter alloc] initWithTextField:self]; 30 | } 31 | 32 | return self; 33 | } 34 | 35 | - (void)_textDidChange 36 | { 37 | _textWasPasted = NO; 38 | } 39 | 40 | #pragma mark - Accessibility 41 | 42 | - (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement 43 | { 44 | // UITextField is accessible by default (some nested views are) and disabling that is not supported. 45 | // On iOS accessible elements cannot be nested, therefore enabling accessibility for some container view 46 | // (even in a case where this view is a part of public API of TextInput on iOS) shadows some features implemented inside the component. 47 | } 48 | 49 | #pragma mark - Properties 50 | 51 | - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset 52 | { 53 | _textContainerInset = textContainerInset; 54 | [self setNeedsLayout]; 55 | } 56 | 57 | - (void)setPlaceholder:(NSString *)placeholder 58 | { 59 | [super setPlaceholder:placeholder]; 60 | [self _updatePlaceholder]; 61 | } 62 | 63 | - (void)setPlaceholderColor:(UIColor *)placeholderColor 64 | { 65 | _placeholderColor = placeholderColor; 66 | [self _updatePlaceholder]; 67 | } 68 | 69 | - (void)setDefaultTextAttributes:(NSDictionary *)defaultTextAttributes 70 | { 71 | if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) { 72 | return; 73 | } 74 | 75 | _defaultTextAttributes = defaultTextAttributes; 76 | [super setDefaultTextAttributes:defaultTextAttributes]; 77 | [self _updatePlaceholder]; 78 | } 79 | 80 | - (NSDictionary *)defaultTextAttributes 81 | { 82 | return _defaultTextAttributes; 83 | } 84 | 85 | - (void)_updatePlaceholder 86 | { 87 | self.attributedPlaceholder = [[NSAttributedString alloc] initWithString:self.placeholder ?: @"" 88 | attributes:[self _placeholderTextAttributes]]; 89 | } 90 | 91 | - (BOOL)isEditable 92 | { 93 | return self.isEnabled; 94 | } 95 | 96 | - (void)setEditable:(BOOL)editable 97 | { 98 | self.enabled = editable; 99 | } 100 | 101 | - (void)setScrollEnabled:(BOOL)enabled 102 | { 103 | // Do noting, compatible with multiline textinput 104 | } 105 | 106 | - (BOOL)scrollEnabled 107 | { 108 | return NO; 109 | } 110 | 111 | - (void)setSecureTextEntry:(BOOL)secureTextEntry 112 | { 113 | if (self.secureTextEntry == secureTextEntry) { 114 | return; 115 | } 116 | 117 | [super setSecureTextEntry:secureTextEntry]; 118 | 119 | // Fix for trailing whitespate issue 120 | // Read more: 121 | // https://stackoverflow.com/questions/14220187/uitextfield-has-trailing-whitespace-after-securetextentry-toggle/22537788#22537788 122 | NSAttributedString *originalText = [self.attributedText copy]; 123 | self.attributedText = [NSAttributedString new]; 124 | self.attributedText = originalText; 125 | } 126 | 127 | #pragma mark - Placeholder 128 | 129 | - (NSDictionary *)_placeholderTextAttributes 130 | { 131 | NSMutableDictionary *textAttributes = [_defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new]; 132 | 133 | if (self.placeholderColor) { 134 | [textAttributes setValue:self.placeholderColor forKey:NSForegroundColorAttributeName]; 135 | } else { 136 | [textAttributes removeObjectForKey:NSForegroundColorAttributeName]; 137 | } 138 | 139 | return textAttributes; 140 | } 141 | 142 | #pragma mark - Context Menu 143 | 144 | - (BOOL)canPerformAction:(SEL)action withSender:(id)sender 145 | { 146 | if (_contextMenuHidden) { 147 | return NO; 148 | } 149 | 150 | return [super canPerformAction:action withSender:sender]; 151 | } 152 | 153 | #pragma mark - Caret Manipulation 154 | 155 | - (CGRect)caretRectForPosition:(UITextPosition *)position 156 | { 157 | if (_caretHidden) { 158 | return CGRectZero; 159 | } 160 | 161 | return [super caretRectForPosition:position]; 162 | } 163 | 164 | #pragma mark - Positioning Overrides 165 | 166 | - (CGRect)textRectForBounds:(CGRect)bounds 167 | { 168 | return UIEdgeInsetsInsetRect([super textRectForBounds:bounds], _textContainerInset); 169 | } 170 | 171 | - (CGRect)editingRectForBounds:(CGRect)bounds 172 | { 173 | return [self textRectForBounds:bounds]; 174 | } 175 | 176 | #pragma mark - Overrides 177 | 178 | #pragma clang diagnostic push 179 | #pragma clang diagnostic ignored "-Wdeprecated-implementations" 180 | // Overrides selectedTextRange setter to get notify when selectedTextRange changed. 181 | - (void)setSelectedTextRange:(UITextRange *)selectedTextRange 182 | { 183 | [super setSelectedTextRange:selectedTextRange]; 184 | [_textInputDelegateAdapter selectedTextRangeWasSet]; 185 | } 186 | #pragma clang diagnostic pop 187 | 188 | - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate 189 | { 190 | if (!notifyDelegate) { 191 | // We have to notify an adapter that following selection change was initiated programmatically, 192 | // so the adapter must not generate a notification for it. 193 | [_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange]; 194 | } 195 | 196 | [super setSelectedTextRange:selectedTextRange]; 197 | } 198 | 199 | - (void)paste:(id)sender 200 | { 201 | [super paste:sender]; 202 | _textWasPasted = YES; 203 | } 204 | 205 | #pragma mark - Layout 206 | 207 | - (CGSize)contentSize 208 | { 209 | // Returning size DOES contain `textContainerInset` (aka `padding`). 210 | return self.intrinsicContentSize; 211 | } 212 | 213 | - (CGSize)intrinsicContentSize 214 | { 215 | // Note: `placeholder` defines intrinsic size for ``. 216 | NSString *text = self.placeholder ?: @""; 217 | CGSize size = [text sizeWithAttributes:[self _placeholderTextAttributes]]; 218 | size = CGSizeMake(RCTCeilPixelValue(size.width), RCTCeilPixelValue(size.height)); 219 | size.width += _textContainerInset.left + _textContainerInset.right; 220 | size.height += _textContainerInset.top + _textContainerInset.bottom; 221 | // Returning size DOES contain `textContainerInset` (aka `padding`). 222 | return size; 223 | } 224 | 225 | - (CGSize)sizeThatFits:(CGSize)size 226 | { 227 | // All size values here contain `textContainerInset` (aka `padding`). 228 | CGSize intrinsicSize = self.intrinsicContentSize; 229 | return CGSizeMake(MIN(size.width, intrinsicSize.width), MIN(size.height, intrinsicSize.height)); 230 | } 231 | 232 | @end -------------------------------------------------------------------------------- /patches/RCTUITextView.m: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | #import 9 | 10 | #import 11 | #import 12 | 13 | #import 14 | #import 15 | 16 | @implementation RCTUITextView 17 | { 18 | UILabel *_placeholderView; 19 | UITextView *_detachedTextView; 20 | RCTBackedTextViewDelegateAdapter *_textInputDelegateAdapter; 21 | NSDictionary *_defaultTextAttributes; 22 | } 23 | 24 | static UIFont *defaultPlaceholderFont() 25 | { 26 | return [UIFont systemFontOfSize:17]; 27 | } 28 | 29 | static UIColor *defaultPlaceholderColor() 30 | { 31 | // Default placeholder color from UITextField. 32 | return [UIColor colorWithRed:0 green:0 blue:0.0980392 alpha:0.22]; 33 | } 34 | 35 | - (instancetype)initWithFrame:(CGRect)frame 36 | { 37 | if (self = [super initWithFrame:frame]) { 38 | [[NSNotificationCenter defaultCenter] addObserver:self 39 | selector:@selector(textDidChange) 40 | name:UITextViewTextDidChangeNotification 41 | object:self]; 42 | 43 | _placeholderView = [[UILabel alloc] initWithFrame:self.bounds]; 44 | _placeholderView.isAccessibilityElement = NO; 45 | _placeholderView.numberOfLines = 0; 46 | [self addSubview:_placeholderView]; 47 | 48 | _textInputDelegateAdapter = [[RCTBackedTextViewDelegateAdapter alloc] initWithTextView:self]; 49 | 50 | self.backgroundColor = [UIColor clearColor]; 51 | self.textColor = [UIColor blackColor]; 52 | // This line actually removes 5pt (default value) left and right padding in UITextView. 53 | self.textContainer.lineFragmentPadding = 0; 54 | #if !TARGET_OS_TV 55 | self.scrollsToTop = NO; 56 | #endif 57 | self.scrollEnabled = YES; 58 | } 59 | 60 | return self; 61 | } 62 | 63 | #pragma mark - Accessibility 64 | 65 | - (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement 66 | { 67 | // UITextView is accessible by default (some nested views are) and disabling that is not supported. 68 | // On iOS accessible elements cannot be nested, therefore enabling accessibility for some container view 69 | // (even in a case where this view is a part of public API of TextInput on iOS) shadows some features implemented inside the component. 70 | } 71 | 72 | - (NSString *)accessibilityLabel 73 | { 74 | NSMutableString *accessibilityLabel = [NSMutableString new]; 75 | 76 | NSString *superAccessibilityLabel = [super accessibilityLabel]; 77 | if (superAccessibilityLabel.length > 0) { 78 | [accessibilityLabel appendString:superAccessibilityLabel]; 79 | } 80 | 81 | if (self.placeholder.length > 0 && self.attributedText.string.length == 0) { 82 | if (accessibilityLabel.length > 0) { 83 | [accessibilityLabel appendString:@" "]; 84 | } 85 | [accessibilityLabel appendString:self.placeholder]; 86 | } 87 | 88 | return accessibilityLabel; 89 | } 90 | 91 | #pragma mark - Properties 92 | 93 | - (void)setPlaceholder:(NSString *)placeholder 94 | { 95 | _placeholder = placeholder; 96 | [self _updatePlaceholder]; 97 | } 98 | 99 | - (void)setPlaceholderColor:(UIColor *)placeholderColor 100 | { 101 | _placeholderColor = placeholderColor; 102 | [self _updatePlaceholder]; 103 | } 104 | 105 | - (void)setDefaultTextAttributes:(NSDictionary *)defaultTextAttributes 106 | { 107 | if ([_defaultTextAttributes isEqualToDictionary:defaultTextAttributes]) { 108 | return; 109 | } 110 | 111 | _defaultTextAttributes = defaultTextAttributes; 112 | self.typingAttributes = defaultTextAttributes; 113 | [self _updatePlaceholder]; 114 | } 115 | 116 | - (NSDictionary *)defaultTextAttributes 117 | { 118 | return _defaultTextAttributes; 119 | } 120 | 121 | - (void)textDidChange 122 | { 123 | _textWasPasted = NO; 124 | [self _invalidatePlaceholderVisibility]; 125 | } 126 | 127 | #pragma mark - Overrides 128 | 129 | - (void)setFont:(UIFont *)font 130 | { 131 | [super setFont:font]; 132 | [self _updatePlaceholder]; 133 | } 134 | 135 | - (void)setTextAlignment:(NSTextAlignment)textAlignment 136 | { 137 | [super setTextAlignment:textAlignment]; 138 | _placeholderView.textAlignment = textAlignment; 139 | } 140 | 141 | - (void)setAttributedText:(NSAttributedString *)attributedText 142 | { 143 | // Using `setAttributedString:` while user is typing breaks some internal mechanics 144 | // when entering complex input languages such as Chinese, Korean or Japanese. 145 | // see: https://github.com/facebook/react-native/issues/19339 146 | 147 | // We try to avoid calling this method as much as we can. 148 | // If the text has changed, there is nothing we can do. 149 | if (![super.attributedText.string isEqualToString:attributedText.string]) { 150 | [super setAttributedText:attributedText]; 151 | } else { 152 | // But if the text is preserved, we just copying the attributes from the source string. 153 | if (![super.attributedText isEqualToAttributedString:attributedText]) { 154 | [self copyTextAttributesFrom:attributedText]; 155 | } 156 | } 157 | 158 | [self textDidChange]; 159 | } 160 | 161 | #pragma mark - Overrides 162 | 163 | - (void)setSelectedTextRange:(UITextRange *)selectedTextRange notifyDelegate:(BOOL)notifyDelegate 164 | { 165 | if (!notifyDelegate) { 166 | // We have to notify an adapter that following selection change was initiated programmatically, 167 | // so the adapter must not generate a notification for it. 168 | [_textInputDelegateAdapter skipNextTextInputDidChangeSelectionEventWithTextRange:selectedTextRange]; 169 | } 170 | 171 | [super setSelectedTextRange:selectedTextRange]; 172 | } 173 | 174 | - (void)paste:(id)sender 175 | { 176 | [super paste:sender]; 177 | _textWasPasted = YES; 178 | } 179 | 180 | - (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated 181 | { 182 | // Turning off scroll animation. 183 | // This fixes the problem also known as "flaky scrolling". 184 | [super setContentOffset:contentOffset animated:NO]; 185 | } 186 | 187 | #pragma mark - Layout 188 | 189 | - (CGFloat)preferredMaxLayoutWidth 190 | { 191 | // Returning size DOES contain `textContainerInset` (aka `padding`). 192 | return _preferredMaxLayoutWidth ?: self.placeholderSize.width; 193 | } 194 | 195 | - (CGSize)placeholderSize 196 | { 197 | UIEdgeInsets textContainerInset = self.textContainerInset; 198 | NSString *placeholder = self.placeholder ?: @""; 199 | CGSize maxPlaceholderSize = CGSizeMake(UIEdgeInsetsInsetRect(self.bounds, textContainerInset).size.width, CGFLOAT_MAX); 200 | CGSize placeholderSize = [placeholder boundingRectWithSize:maxPlaceholderSize options:NSStringDrawingUsesLineFragmentOrigin attributes:[self _placeholderTextAttributes] context:nil].size; 201 | placeholderSize = CGSizeMake(RCTCeilPixelValue(placeholderSize.width), RCTCeilPixelValue(placeholderSize.height)); 202 | placeholderSize.width += textContainerInset.left + textContainerInset.right; 203 | placeholderSize.height += textContainerInset.top + textContainerInset.bottom; 204 | // Returning size DOES contain `textContainerInset` (aka `padding`; as `sizeThatFits:` does). 205 | return placeholderSize; 206 | } 207 | 208 | - (CGSize)contentSize 209 | { 210 | CGSize contentSize = super.contentSize; 211 | CGSize placeholderSize = _placeholderView.isHidden ? CGSizeZero : self.placeholderSize; 212 | // When a text input is empty, it actually displays a placehoder. 213 | // So, we have to consider `placeholderSize` as a minimum `contentSize`. 214 | // Returning size DOES contain `textContainerInset` (aka `padding`). 215 | return CGSizeMake( 216 | MAX(contentSize.width, placeholderSize.width), 217 | MAX(contentSize.height, placeholderSize.height)); 218 | } 219 | 220 | - (void)layoutSubviews 221 | { 222 | [super layoutSubviews]; 223 | 224 | CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, self.textContainerInset); 225 | CGFloat placeholderHeight = [_placeholderView sizeThatFits:textFrame.size].height; 226 | textFrame.size.height = MIN(placeholderHeight, textFrame.size.height); 227 | _placeholderView.frame = textFrame; 228 | } 229 | 230 | - (CGSize)intrinsicContentSize 231 | { 232 | // Returning size DOES contain `textContainerInset` (aka `padding`). 233 | return [self sizeThatFits:CGSizeMake(self.preferredMaxLayoutWidth, CGFLOAT_MAX)]; 234 | } 235 | 236 | - (CGSize)sizeThatFits:(CGSize)size 237 | { 238 | // Returned fitting size depends on text size and placeholder size. 239 | CGSize textSize = [super sizeThatFits:size]; 240 | CGSize placeholderSize = self.placeholderSize; 241 | // Returning size DOES contain `textContainerInset` (aka `padding`). 242 | return CGSizeMake(MAX(textSize.width, placeholderSize.width), MAX(textSize.height, placeholderSize.height)); 243 | } 244 | 245 | #pragma mark - Context Menu 246 | 247 | - (BOOL)canPerformAction:(SEL)action withSender:(id)sender 248 | { 249 | if (_contextMenuHidden) { 250 | return NO; 251 | } 252 | 253 | return [super canPerformAction:action withSender:sender]; 254 | } 255 | 256 | #pragma mark - Placeholder 257 | 258 | - (void)_invalidatePlaceholderVisibility 259 | { 260 | BOOL isVisible = _placeholder.length != 0 && self.attributedText.length == 0; 261 | _placeholderView.hidden = !isVisible; 262 | } 263 | 264 | - (void)_updatePlaceholder 265 | { 266 | _placeholderView.attributedText = [[NSAttributedString alloc] initWithString:_placeholder ?: @"" attributes:[self _placeholderTextAttributes]]; 267 | } 268 | 269 | - (NSDictionary *)_placeholderTextAttributes 270 | { 271 | NSMutableDictionary *textAttributes = [_defaultTextAttributes mutableCopy] ?: [NSMutableDictionary new]; 272 | 273 | [textAttributes setValue:self.placeholderColor ?: defaultPlaceholderColor() forKey:NSForegroundColorAttributeName]; 274 | 275 | if (![textAttributes objectForKey:NSFontAttributeName]) { 276 | [textAttributes setValue:defaultPlaceholderFont() forKey:NSFontAttributeName]; 277 | } 278 | 279 | return textAttributes; 280 | } 281 | 282 | #pragma mark - Utility Methods 283 | 284 | - (void)copyTextAttributesFrom:(NSAttributedString *)sourceString 285 | { 286 | [self.textStorage beginEditing]; 287 | 288 | NSTextStorage *textStorage = self.textStorage; 289 | [sourceString enumerateAttributesInRange:NSMakeRange(0, sourceString.length) 290 | options:NSAttributedStringEnumerationReverse 291 | usingBlock:^(NSDictionary * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) { 292 | [textStorage setAttributes:attrs range:range]; 293 | }]; 294 | 295 | [self.textStorage endEditing]; 296 | } 297 | 298 | @end --------------------------------------------------------------------------------