RCTBaseTextInputView.m 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  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. #import <React/RCTBaseTextInputView.h>
  8. #import <React/RCTBridge.h>
  9. #import <React/RCTConvert.h>
  10. #import <React/RCTEventDispatcher.h>
  11. #import <React/RCTUIManager.h>
  12. #import <React/RCTUtils.h>
  13. #import <React/UIView+React.h>
  14. #import <React/RCTInputAccessoryView.h>
  15. #import <React/RCTInputAccessoryViewContent.h>
  16. #import <React/RCTTextAttributes.h>
  17. #import <React/RCTTextSelection.h>
  18. @implementation RCTBaseTextInputView {
  19. __weak RCTBridge *_bridge;
  20. __weak RCTEventDispatcher *_eventDispatcher;
  21. BOOL _hasInputAccesoryView;
  22. NSString *_Nullable _predictedText;
  23. BOOL _didMoveToWindow;
  24. }
  25. - (instancetype)initWithBridge:(RCTBridge *)bridge
  26. {
  27. RCTAssertParam(bridge);
  28. if (self = [super initWithFrame:CGRectZero]) {
  29. _bridge = bridge;
  30. _eventDispatcher = bridge.eventDispatcher;
  31. }
  32. return self;
  33. }
  34. RCT_NOT_IMPLEMENTED(- (instancetype)init)
  35. RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)decoder)
  36. RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
  37. - (UIView<RCTBackedTextInputViewProtocol> *)backedTextInputView
  38. {
  39. RCTAssert(NO, @"-[RCTBaseTextInputView backedTextInputView] must be implemented in subclass.");
  40. return nil;
  41. }
  42. #pragma mark - RCTComponent
  43. - (void)didUpdateReactSubviews
  44. {
  45. // Do nothing.
  46. }
  47. #pragma mark - Properties
  48. - (void)setTextAttributes:(RCTTextAttributes *)textAttributes
  49. {
  50. _textAttributes = textAttributes;
  51. [self enforceTextAttributesIfNeeded];
  52. }
  53. - (void)enforceTextAttributesIfNeeded
  54. {
  55. id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
  56. NSDictionary<NSAttributedStringKey,id> *textAttributes = [[_textAttributes effectiveTextAttributes] mutableCopy];
  57. if ([textAttributes valueForKey:NSForegroundColorAttributeName] == nil) {
  58. [textAttributes setValue:[UIColor blackColor] forKey:NSForegroundColorAttributeName];
  59. }
  60. backedTextInputView.defaultTextAttributes = textAttributes;
  61. }
  62. - (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets
  63. {
  64. _reactPaddingInsets = reactPaddingInsets;
  65. // We apply `paddingInsets` as `backedTextInputView`'s `textContainerInset`.
  66. self.backedTextInputView.textContainerInset = reactPaddingInsets;
  67. [self setNeedsLayout];
  68. }
  69. - (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets
  70. {
  71. _reactBorderInsets = reactBorderInsets;
  72. // We apply `borderInsets` as `backedTextInputView` layout offset.
  73. self.backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets);
  74. [self setNeedsLayout];
  75. }
  76. - (NSAttributedString *)attributedText
  77. {
  78. return self.backedTextInputView.attributedText;
  79. }
  80. - (BOOL)textOf:(NSAttributedString*)newText equals:(NSAttributedString*)oldText{
  81. // When the dictation is running we can't update the attributed text on the backed up text view
  82. // because setting the attributed string will kill the dictation. This means that we can't impose
  83. // the settings on a dictation.
  84. // Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the
  85. // text that we should disregard. See https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc
  86. // for more info.
  87. // If the user added an emoji, the system adds a font attribute for the emoji and stores the original font in NSOriginalFont.
  88. // Lastly, when entering a password, etc., there will be additional styling on the field as the native text view
  89. // handles showing the last character for a split second.
  90. __block BOOL fontHasBeenUpdatedBySystem = false;
  91. [oldText enumerateAttribute:@"NSOriginalFont" inRange:NSMakeRange(0, oldText.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
  92. if (value){
  93. fontHasBeenUpdatedBySystem = true;
  94. }
  95. }];
  96. BOOL shouldFallbackToBareTextComparison =
  97. [self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] ||
  98. self.backedTextInputView.markedTextRange ||
  99. self.backedTextInputView.isSecureTextEntry ||
  100. fontHasBeenUpdatedBySystem;
  101. if (shouldFallbackToBareTextComparison) {
  102. return ([newText.string isEqualToString:oldText.string]);
  103. } else {
  104. return ([newText isEqualToAttributedString:oldText]);
  105. }
  106. }
  107. - (void)setAttributedText:(NSAttributedString *)attributedText
  108. {
  109. NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
  110. BOOL textNeedsUpdate = NO;
  111. // Remove tag attribute to ensure correct attributed string comparison.
  112. NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy];
  113. NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy];
  114. [backedTextInputViewTextCopy removeAttribute:RCTTextAttributesTagAttributeName
  115. range:NSMakeRange(0, backedTextInputViewTextCopy.length)];
  116. [attributedTextCopy removeAttribute:RCTTextAttributesTagAttributeName
  117. range:NSMakeRange(0, attributedTextCopy.length)];
  118. textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO);
  119. if (eventLag == 0 && textNeedsUpdate) {
  120. UITextRange *selection = self.backedTextInputView.selectedTextRange;
  121. NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length;
  122. self.backedTextInputView.attributedText = attributedText;
  123. if (selection.empty) {
  124. // Maintaining a cursor position relative to the end of the old text.
  125. NSInteger offsetStart =
  126. [self.backedTextInputView offsetFromPosition:self.backedTextInputView.beginningOfDocument
  127. toPosition:selection.start];
  128. NSInteger offsetFromEnd = oldTextLength - offsetStart;
  129. NSInteger newOffset = attributedText.string.length - offsetFromEnd;
  130. UITextPosition *position =
  131. [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
  132. offset:newOffset];
  133. [self.backedTextInputView setSelectedTextRange:[self.backedTextInputView textRangeFromPosition:position toPosition:position]
  134. notifyDelegate:YES];
  135. }
  136. [self updateLocalData];
  137. } else if (eventLag > RCTTextUpdateLagWarningThreshold) {
  138. RCTLog(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", self.backedTextInputView.attributedText.string, (long long)eventLag);
  139. }
  140. }
  141. - (RCTTextSelection *)selection
  142. {
  143. id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
  144. UITextRange *selectedTextRange = backedTextInputView.selectedTextRange;
  145. return [[RCTTextSelection new] initWithStart:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.start]
  146. end:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.end]];
  147. }
  148. - (void)setSelection:(RCTTextSelection *)selection
  149. {
  150. if (!selection) {
  151. return;
  152. }
  153. id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
  154. UITextRange *previousSelectedTextRange = backedTextInputView.selectedTextRange;
  155. UITextPosition *start = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.start];
  156. UITextPosition *end = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.end];
  157. UITextRange *selectedTextRange = [backedTextInputView textRangeFromPosition:start toPosition:end];
  158. NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
  159. if (eventLag == 0 && ![previousSelectedTextRange isEqual:selectedTextRange]) {
  160. [backedTextInputView setSelectedTextRange:selectedTextRange notifyDelegate:NO];
  161. } else if (eventLag > RCTTextUpdateLagWarningThreshold) {
  162. RCTLog(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", backedTextInputView.attributedText.string, (long long)eventLag);
  163. }
  164. }
  165. - (void)setSelectionStart:(NSInteger)start
  166. selectionEnd:(NSInteger)end
  167. {
  168. UITextPosition *startPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
  169. offset:start];
  170. UITextPosition *endPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
  171. offset:end];
  172. if (startPosition && endPosition) {
  173. UITextRange *range = [self.backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition];
  174. [self.backedTextInputView setSelectedTextRange:range notifyDelegate:NO];
  175. }
  176. }
  177. - (void)setTextContentType:(NSString *)type
  178. {
  179. #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED)
  180. static dispatch_once_t onceToken;
  181. static NSDictionary<NSString *, NSString *> *contentTypeMap;
  182. dispatch_once(&onceToken, ^{
  183. contentTypeMap = @{@"none": @"",
  184. @"URL": UITextContentTypeURL,
  185. @"addressCity": UITextContentTypeAddressCity,
  186. @"addressCityAndState":UITextContentTypeAddressCityAndState,
  187. @"addressState": UITextContentTypeAddressState,
  188. @"countryName": UITextContentTypeCountryName,
  189. @"creditCardNumber": UITextContentTypeCreditCardNumber,
  190. @"emailAddress": UITextContentTypeEmailAddress,
  191. @"familyName": UITextContentTypeFamilyName,
  192. @"fullStreetAddress": UITextContentTypeFullStreetAddress,
  193. @"givenName": UITextContentTypeGivenName,
  194. @"jobTitle": UITextContentTypeJobTitle,
  195. @"location": UITextContentTypeLocation,
  196. @"middleName": UITextContentTypeMiddleName,
  197. @"name": UITextContentTypeName,
  198. @"namePrefix": UITextContentTypeNamePrefix,
  199. @"nameSuffix": UITextContentTypeNameSuffix,
  200. @"nickname": UITextContentTypeNickname,
  201. @"organizationName": UITextContentTypeOrganizationName,
  202. @"postalCode": UITextContentTypePostalCode,
  203. @"streetAddressLine1": UITextContentTypeStreetAddressLine1,
  204. @"streetAddressLine2": UITextContentTypeStreetAddressLine2,
  205. @"sublocality": UITextContentTypeSublocality,
  206. @"telephoneNumber": UITextContentTypeTelephoneNumber,
  207. };
  208. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
  209. if (@available(iOS 11.0, tvOS 11.0, *)) {
  210. NSDictionary<NSString *, NSString *> * iOS11extras = @{@"username": UITextContentTypeUsername,
  211. @"password": UITextContentTypePassword};
  212. NSMutableDictionary<NSString *, NSString *> * iOS11baseMap = [contentTypeMap mutableCopy];
  213. [iOS11baseMap addEntriesFromDictionary:iOS11extras];
  214. contentTypeMap = [iOS11baseMap copy];
  215. }
  216. #endif
  217. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000 /* __IPHONE_12_0 */
  218. if (@available(iOS 12.0, tvOS 12.0, *)) {
  219. NSDictionary<NSString *, NSString *> * iOS12extras = @{@"newPassword": UITextContentTypeNewPassword,
  220. @"oneTimeCode": UITextContentTypeOneTimeCode};
  221. NSMutableDictionary<NSString *, NSString *> * iOS12baseMap = [contentTypeMap mutableCopy];
  222. [iOS12baseMap addEntriesFromDictionary:iOS12extras];
  223. contentTypeMap = [iOS12baseMap copy];
  224. }
  225. #endif
  226. });
  227. // Setting textContentType to an empty string will disable any
  228. // default behaviour, like the autofill bar for password inputs
  229. self.backedTextInputView.textContentType = contentTypeMap[type] ?: type;
  230. #endif
  231. }
  232. - (void)setPasswordRules:(NSString *)descriptor
  233. {
  234. #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0
  235. if (@available(iOS 12.0, *)) {
  236. self.backedTextInputView.passwordRules = [UITextInputPasswordRules passwordRulesWithDescriptor:descriptor];
  237. }
  238. #endif
  239. }
  240. - (UIKeyboardType)keyboardType
  241. {
  242. return self.backedTextInputView.keyboardType;
  243. }
  244. - (void)setKeyboardType:(UIKeyboardType)keyboardType
  245. {
  246. UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
  247. if (textInputView.keyboardType != keyboardType) {
  248. textInputView.keyboardType = keyboardType;
  249. // Without the call to reloadInputViews, the keyboard will not change until the textview field (the first responder) loses and regains focus.
  250. if (textInputView.isFirstResponder) {
  251. [textInputView reloadInputViews];
  252. }
  253. }
  254. }
  255. #pragma mark - RCTBackedTextInputDelegate
  256. - (BOOL)textInputShouldBeginEditing
  257. {
  258. return YES;
  259. }
  260. - (void)textInputDidBeginEditing
  261. {
  262. if (_clearTextOnFocus) {
  263. self.backedTextInputView.attributedText = [NSAttributedString new];
  264. }
  265. if (_selectTextOnFocus) {
  266. [self.backedTextInputView selectAll:nil];
  267. }
  268. [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
  269. reactTag:self.reactTag
  270. text:self.backedTextInputView.attributedText.string
  271. key:nil
  272. eventCount:_nativeEventCount];
  273. }
  274. - (BOOL)textInputShouldEndEditing
  275. {
  276. return YES;
  277. }
  278. - (void)textInputDidEndEditing
  279. {
  280. [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
  281. reactTag:self.reactTag
  282. text:self.backedTextInputView.attributedText.string
  283. key:nil
  284. eventCount:_nativeEventCount];
  285. [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
  286. reactTag:self.reactTag
  287. text:self.backedTextInputView.attributedText.string
  288. key:nil
  289. eventCount:_nativeEventCount];
  290. }
  291. - (BOOL)textInputShouldReturn
  292. {
  293. // We send `submit` event here, in `textInputShouldReturn`
  294. // (not in `textInputDidReturn)`, because of semantic of the event:
  295. // `onSubmitEditing` is called when "Submit" button
  296. // (the blue key on onscreen keyboard) did pressed
  297. // (no connection to any specific "submitting" process).
  298. [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit
  299. reactTag:self.reactTag
  300. text:self.backedTextInputView.attributedText.string
  301. key:nil
  302. eventCount:_nativeEventCount];
  303. return _blurOnSubmit;
  304. }
  305. - (void)textInputDidReturn
  306. {
  307. // Does nothing.
  308. }
  309. - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
  310. {
  311. id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
  312. if (!backedTextInputView.textWasPasted) {
  313. [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
  314. reactTag:self.reactTag
  315. text:nil
  316. key:text
  317. eventCount:_nativeEventCount];
  318. }
  319. if (_maxLength) {
  320. NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length, 0);
  321. if (text.length > allowedLength) {
  322. // If we typed/pasted more than one character, limit the text inputted.
  323. if (text.length > 1) {
  324. // Truncate the input string so the result is exactly maxLength
  325. NSString *limitedString = [text substringToIndex:allowedLength];
  326. NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy];
  327. // Apply text attributes if original input view doesn't have text.
  328. if (backedTextInputView.attributedText.length == 0) {
  329. newAttributedText = [[NSMutableAttributedString alloc] initWithString:[self.textAttributes applyTextAttributesToText:limitedString] attributes:self.textAttributes.effectiveTextAttributes];
  330. } else {
  331. [newAttributedText replaceCharactersInRange:range withString:limitedString];
  332. }
  333. backedTextInputView.attributedText = newAttributedText;
  334. _predictedText = newAttributedText.string;
  335. // Collapse selection at end of insert to match normal paste behavior.
  336. UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument
  337. offset:(range.location + allowedLength)];
  338. [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd]
  339. notifyDelegate:YES];
  340. [self textInputDidChange];
  341. }
  342. return nil; // Rejecting the change.
  343. }
  344. }
  345. NSString *previousText = backedTextInputView.attributedText.string ?: @"";
  346. if (range.location + range.length > backedTextInputView.attributedText.string.length) {
  347. _predictedText = backedTextInputView.attributedText.string;
  348. } else {
  349. _predictedText = [backedTextInputView.attributedText.string stringByReplacingCharactersInRange:range withString:text];
  350. }
  351. if (_onTextInput) {
  352. _onTextInput(@{
  353. @"text": text,
  354. @"previousText": previousText,
  355. @"range": @{
  356. @"start": @(range.location),
  357. @"end": @(range.location + range.length)
  358. },
  359. @"eventCount": @(_nativeEventCount),
  360. });
  361. }
  362. return text; // Accepting the change.
  363. }
  364. - (void)textInputDidChange
  365. {
  366. [self updateLocalData];
  367. id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
  368. // Detect when `backedTextInputView` updates happened that didn't invoke `shouldChangeTextInRange`
  369. // (e.g. typing simplified Chinese in pinyin will insert and remove spaces without
  370. // calling shouldChangeTextInRange). This will cause JS to get out of sync so we
  371. // update the mismatched range.
  372. NSRange currentRange;
  373. NSRange predictionRange;
  374. if (findMismatch(backedTextInputView.attributedText.string, _predictedText, &currentRange, &predictionRange)) {
  375. NSString *replacement = [backedTextInputView.attributedText.string substringWithRange:currentRange];
  376. [self textInputShouldChangeText:replacement inRange:predictionRange];
  377. // JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it.
  378. [self textInputDidChangeSelection];
  379. }
  380. _nativeEventCount++;
  381. if (_onChange) {
  382. _onChange(@{
  383. @"text": self.attributedText.string,
  384. @"target": self.reactTag,
  385. @"eventCount": @(_nativeEventCount),
  386. });
  387. }
  388. }
  389. - (void)textInputDidChangeSelection
  390. {
  391. if (!_onSelectionChange) {
  392. return;
  393. }
  394. RCTTextSelection *selection = self.selection;
  395. _onSelectionChange(@{
  396. @"selection": @{
  397. @"start": @(selection.start),
  398. @"end": @(selection.end),
  399. },
  400. });
  401. }
  402. - (void)updateLocalData
  403. {
  404. [self enforceTextAttributesIfNeeded];
  405. [_bridge.uiManager setLocalData:[self.backedTextInputView.attributedText copy]
  406. forView:self];
  407. }
  408. #pragma mark - Layout (in UIKit terms, with all insets)
  409. - (CGSize)intrinsicContentSize
  410. {
  411. CGSize size = self.backedTextInputView.intrinsicContentSize;
  412. size.width += _reactBorderInsets.left + _reactBorderInsets.right;
  413. size.height += _reactBorderInsets.top + _reactBorderInsets.bottom;
  414. // Returning value DOES include border and padding insets.
  415. return size;
  416. }
  417. - (CGSize)sizeThatFits:(CGSize)size
  418. {
  419. CGFloat compoundHorizontalBorderInset = _reactBorderInsets.left + _reactBorderInsets.right;
  420. CGFloat compoundVerticalBorderInset = _reactBorderInsets.top + _reactBorderInsets.bottom;
  421. size.width -= compoundHorizontalBorderInset;
  422. size.height -= compoundVerticalBorderInset;
  423. // Note: `paddingInsets` was already included in `backedTextInputView` size
  424. // because it was applied as `textContainerInset`.
  425. CGSize fittingSize = [self.backedTextInputView sizeThatFits:size];
  426. fittingSize.width += compoundHorizontalBorderInset;
  427. fittingSize.height += compoundVerticalBorderInset;
  428. // Returning value DOES include border and padding insets.
  429. return fittingSize;
  430. }
  431. #pragma mark - Accessibility
  432. - (UIView *)reactAccessibilityElement
  433. {
  434. return self.backedTextInputView;
  435. }
  436. #pragma mark - Focus Control
  437. - (void)reactFocus
  438. {
  439. [self.backedTextInputView reactFocus];
  440. }
  441. - (void)reactBlur
  442. {
  443. [self.backedTextInputView reactBlur];
  444. }
  445. - (void)didMoveToWindow
  446. {
  447. if (self.autoFocus && !_didMoveToWindow) {
  448. [self.backedTextInputView reactFocus];
  449. } else {
  450. [self.backedTextInputView reactFocusIfNeeded];
  451. }
  452. _didMoveToWindow = YES;
  453. }
  454. #pragma mark - Custom Input Accessory View
  455. - (void)didSetProps:(NSArray<NSString *> *)changedProps
  456. {
  457. if ([changedProps containsObject:@"inputAccessoryViewID"] && self.inputAccessoryViewID) {
  458. [self setCustomInputAccessoryViewWithNativeID:self.inputAccessoryViewID];
  459. } else if (!self.inputAccessoryViewID) {
  460. [self setDefaultInputAccessoryView];
  461. }
  462. }
  463. - (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID
  464. {
  465. #if !TARGET_OS_TV
  466. __weak RCTBaseTextInputView *weakSelf = self;
  467. [_bridge.uiManager rootViewForReactTag:self.reactTag withCompletion:^(UIView *rootView) {
  468. RCTBaseTextInputView *strongSelf = weakSelf;
  469. if (rootView) {
  470. UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID
  471. withRootTag:rootView.reactTag];
  472. if (accessoryView && [accessoryView isKindOfClass:[RCTInputAccessoryView class]]) {
  473. strongSelf.backedTextInputView.inputAccessoryView = ((RCTInputAccessoryView *)accessoryView).inputAccessoryView;
  474. [strongSelf reloadInputViewsIfNecessary];
  475. }
  476. }
  477. }];
  478. #endif /* !TARGET_OS_TV */
  479. }
  480. - (void)setDefaultInputAccessoryView
  481. {
  482. #if !TARGET_OS_TV
  483. UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
  484. UIKeyboardType keyboardType = textInputView.keyboardType;
  485. // These keyboard types (all are number pads) don't have a "Done" button by default,
  486. // so we create an `inputAccessoryView` with this button for them.
  487. BOOL shouldHaveInputAccesoryView;
  488. if (@available(iOS 10.0, *)) {
  489. shouldHaveInputAccesoryView =
  490. (
  491. keyboardType == UIKeyboardTypeNumberPad ||
  492. keyboardType == UIKeyboardTypePhonePad ||
  493. keyboardType == UIKeyboardTypeDecimalPad ||
  494. keyboardType == UIKeyboardTypeASCIICapableNumberPad
  495. ) &&
  496. textInputView.returnKeyType == UIReturnKeyDone;
  497. } else {
  498. shouldHaveInputAccesoryView =
  499. (
  500. keyboardType == UIKeyboardTypeNumberPad ||
  501. keyboardType == UIKeyboardTypePhonePad ||
  502. keyboardType == UIKeyboardTypeDecimalPad
  503. ) &&
  504. textInputView.returnKeyType == UIReturnKeyDone;
  505. }
  506. if (_hasInputAccesoryView == shouldHaveInputAccesoryView) {
  507. return;
  508. }
  509. _hasInputAccesoryView = shouldHaveInputAccesoryView;
  510. if (shouldHaveInputAccesoryView) {
  511. UIToolbar *toolbarView = [[UIToolbar alloc] init];
  512. [toolbarView sizeToFit];
  513. UIBarButtonItem *flexibleSpace =
  514. [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
  515. target:nil
  516. action:nil];
  517. UIBarButtonItem *doneButton =
  518. [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
  519. target:self
  520. action:@selector(handleInputAccessoryDoneButton)];
  521. toolbarView.items = @[flexibleSpace, doneButton];
  522. textInputView.inputAccessoryView = toolbarView;
  523. }
  524. else {
  525. textInputView.inputAccessoryView = nil;
  526. }
  527. [self reloadInputViewsIfNecessary];
  528. #endif /* !TARGET_OS_TV */
  529. }
  530. - (void)reloadInputViewsIfNecessary
  531. {
  532. // We have to call `reloadInputViews` for focused text inputs to update an accessory view.
  533. if (self.backedTextInputView.isFirstResponder) {
  534. [self.backedTextInputView reloadInputViews];
  535. }
  536. }
  537. - (void)handleInputAccessoryDoneButton
  538. {
  539. if ([self textInputShouldReturn]) {
  540. [self.backedTextInputView endEditing:YES];
  541. }
  542. }
  543. #pragma mark - Helpers
  544. static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange)
  545. {
  546. NSInteger firstMismatch = -1;
  547. for (NSUInteger ii = 0; ii < MAX(first.length, second.length); ii++) {
  548. if (ii >= first.length || ii >= second.length || [first characterAtIndex:ii] != [second characterAtIndex:ii]) {
  549. firstMismatch = ii;
  550. break;
  551. }
  552. }
  553. if (firstMismatch == -1) {
  554. return NO;
  555. }
  556. NSUInteger ii = second.length;
  557. NSUInteger lastMismatch = first.length;
  558. while (ii > firstMismatch && lastMismatch > firstMismatch) {
  559. if ([first characterAtIndex:(lastMismatch - 1)] != [second characterAtIndex:(ii - 1)]) {
  560. break;
  561. }
  562. ii--;
  563. lastMismatch--;
  564. }
  565. *firstRange = NSMakeRange(firstMismatch, lastMismatch - firstMismatch);
  566. *secondRange = NSMakeRange(firstMismatch, ii - firstMismatch);
  567. return YES;
  568. }
  569. @end