123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680 |
- /*
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- */
- #import <React/RCTBaseTextInputView.h>
- #import <React/RCTBridge.h>
- #import <React/RCTConvert.h>
- #import <React/RCTEventDispatcher.h>
- #import <React/RCTUIManager.h>
- #import <React/RCTUtils.h>
- #import <React/UIView+React.h>
- #import <React/RCTInputAccessoryView.h>
- #import <React/RCTInputAccessoryViewContent.h>
- #import <React/RCTTextAttributes.h>
- #import <React/RCTTextSelection.h>
- @implementation RCTBaseTextInputView {
- __weak RCTBridge *_bridge;
- __weak RCTEventDispatcher *_eventDispatcher;
- BOOL _hasInputAccesoryView;
- NSString *_Nullable _predictedText;
- BOOL _didMoveToWindow;
- }
- - (instancetype)initWithBridge:(RCTBridge *)bridge
- {
- RCTAssertParam(bridge);
- if (self = [super initWithFrame:CGRectZero]) {
- _bridge = bridge;
- _eventDispatcher = bridge.eventDispatcher;
- }
- return self;
- }
- RCT_NOT_IMPLEMENTED(- (instancetype)init)
- RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)decoder)
- RCT_NOT_IMPLEMENTED(- (instancetype)initWithFrame:(CGRect)frame)
- - (UIView<RCTBackedTextInputViewProtocol> *)backedTextInputView
- {
- RCTAssert(NO, @"-[RCTBaseTextInputView backedTextInputView] must be implemented in subclass.");
- return nil;
- }
- #pragma mark - RCTComponent
- - (void)didUpdateReactSubviews
- {
- // Do nothing.
- }
- #pragma mark - Properties
- - (void)setTextAttributes:(RCTTextAttributes *)textAttributes
- {
- _textAttributes = textAttributes;
- [self enforceTextAttributesIfNeeded];
- }
- - (void)enforceTextAttributesIfNeeded
- {
- id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
- NSDictionary<NSAttributedStringKey,id> *textAttributes = [[_textAttributes effectiveTextAttributes] mutableCopy];
- if ([textAttributes valueForKey:NSForegroundColorAttributeName] == nil) {
- [textAttributes setValue:[UIColor blackColor] forKey:NSForegroundColorAttributeName];
- }
- backedTextInputView.defaultTextAttributes = textAttributes;
- }
- - (void)setReactPaddingInsets:(UIEdgeInsets)reactPaddingInsets
- {
- _reactPaddingInsets = reactPaddingInsets;
- // We apply `paddingInsets` as `backedTextInputView`'s `textContainerInset`.
- self.backedTextInputView.textContainerInset = reactPaddingInsets;
- [self setNeedsLayout];
- }
- - (void)setReactBorderInsets:(UIEdgeInsets)reactBorderInsets
- {
- _reactBorderInsets = reactBorderInsets;
- // We apply `borderInsets` as `backedTextInputView` layout offset.
- self.backedTextInputView.frame = UIEdgeInsetsInsetRect(self.bounds, reactBorderInsets);
- [self setNeedsLayout];
- }
- - (NSAttributedString *)attributedText
- {
- return self.backedTextInputView.attributedText;
- }
- - (BOOL)textOf:(NSAttributedString*)newText equals:(NSAttributedString*)oldText{
- // When the dictation is running we can't update the attributed text on the backed up text view
- // because setting the attributed string will kill the dictation. This means that we can't impose
- // the settings on a dictation.
- // Similarly, when the user is in the middle of inputting some text in Japanese/Chinese, there will be styling on the
- // text that we should disregard. See https://developer.apple.com/documentation/uikit/uitextinput/1614489-markedtextrange?language=objc
- // for more info.
- // If the user added an emoji, the system adds a font attribute for the emoji and stores the original font in NSOriginalFont.
- // Lastly, when entering a password, etc., there will be additional styling on the field as the native text view
- // handles showing the last character for a split second.
- __block BOOL fontHasBeenUpdatedBySystem = false;
- [oldText enumerateAttribute:@"NSOriginalFont" inRange:NSMakeRange(0, oldText.length) options:0 usingBlock:^(id value, NSRange range, BOOL *stop) {
- if (value){
- fontHasBeenUpdatedBySystem = true;
- }
- }];
- BOOL shouldFallbackToBareTextComparison =
- [self.backedTextInputView.textInputMode.primaryLanguage isEqualToString:@"dictation"] ||
- self.backedTextInputView.markedTextRange ||
- self.backedTextInputView.isSecureTextEntry ||
- fontHasBeenUpdatedBySystem;
- if (shouldFallbackToBareTextComparison) {
- return ([newText.string isEqualToString:oldText.string]);
- } else {
- return ([newText isEqualToAttributedString:oldText]);
- }
- }
- - (void)setAttributedText:(NSAttributedString *)attributedText
- {
- NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
- BOOL textNeedsUpdate = NO;
- // Remove tag attribute to ensure correct attributed string comparison.
- NSMutableAttributedString *const backedTextInputViewTextCopy = [self.backedTextInputView.attributedText mutableCopy];
- NSMutableAttributedString *const attributedTextCopy = [attributedText mutableCopy];
- [backedTextInputViewTextCopy removeAttribute:RCTTextAttributesTagAttributeName
- range:NSMakeRange(0, backedTextInputViewTextCopy.length)];
- [attributedTextCopy removeAttribute:RCTTextAttributesTagAttributeName
- range:NSMakeRange(0, attributedTextCopy.length)];
- textNeedsUpdate = ([self textOf:attributedTextCopy equals:backedTextInputViewTextCopy] == NO);
- if (eventLag == 0 && textNeedsUpdate) {
- UITextRange *selection = self.backedTextInputView.selectedTextRange;
- NSInteger oldTextLength = self.backedTextInputView.attributedText.string.length;
- self.backedTextInputView.attributedText = attributedText;
- if (selection.empty) {
- // Maintaining a cursor position relative to the end of the old text.
- NSInteger offsetStart =
- [self.backedTextInputView offsetFromPosition:self.backedTextInputView.beginningOfDocument
- toPosition:selection.start];
- NSInteger offsetFromEnd = oldTextLength - offsetStart;
- NSInteger newOffset = attributedText.string.length - offsetFromEnd;
- UITextPosition *position =
- [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
- offset:newOffset];
- [self.backedTextInputView setSelectedTextRange:[self.backedTextInputView textRangeFromPosition:position toPosition:position]
- notifyDelegate:YES];
- }
- [self updateLocalData];
- } else if (eventLag > RCTTextUpdateLagWarningThreshold) {
- RCTLog(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", self.backedTextInputView.attributedText.string, (long long)eventLag);
- }
- }
- - (RCTTextSelection *)selection
- {
- id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
- UITextRange *selectedTextRange = backedTextInputView.selectedTextRange;
- return [[RCTTextSelection new] initWithStart:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.start]
- end:[backedTextInputView offsetFromPosition:backedTextInputView.beginningOfDocument toPosition:selectedTextRange.end]];
- }
- - (void)setSelection:(RCTTextSelection *)selection
- {
- if (!selection) {
- return;
- }
- id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
- UITextRange *previousSelectedTextRange = backedTextInputView.selectedTextRange;
- UITextPosition *start = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.start];
- UITextPosition *end = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument offset:selection.end];
- UITextRange *selectedTextRange = [backedTextInputView textRangeFromPosition:start toPosition:end];
- NSInteger eventLag = _nativeEventCount - _mostRecentEventCount;
- if (eventLag == 0 && ![previousSelectedTextRange isEqual:selectedTextRange]) {
- [backedTextInputView setSelectedTextRange:selectedTextRange notifyDelegate:NO];
- } else if (eventLag > RCTTextUpdateLagWarningThreshold) {
- RCTLog(@"Native TextInput(%@) is %lld events ahead of JS - try to make your JS faster.", backedTextInputView.attributedText.string, (long long)eventLag);
- }
- }
- - (void)setSelectionStart:(NSInteger)start
- selectionEnd:(NSInteger)end
- {
- UITextPosition *startPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
- offset:start];
- UITextPosition *endPosition = [self.backedTextInputView positionFromPosition:self.backedTextInputView.beginningOfDocument
- offset:end];
- if (startPosition && endPosition) {
- UITextRange *range = [self.backedTextInputView textRangeFromPosition:startPosition toPosition:endPosition];
- [self.backedTextInputView setSelectedTextRange:range notifyDelegate:NO];
- }
- }
- - (void)setTextContentType:(NSString *)type
- {
- #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED)
- static dispatch_once_t onceToken;
- static NSDictionary<NSString *, NSString *> *contentTypeMap;
- dispatch_once(&onceToken, ^{
- contentTypeMap = @{@"none": @"",
- @"URL": UITextContentTypeURL,
- @"addressCity": UITextContentTypeAddressCity,
- @"addressCityAndState":UITextContentTypeAddressCityAndState,
- @"addressState": UITextContentTypeAddressState,
- @"countryName": UITextContentTypeCountryName,
- @"creditCardNumber": UITextContentTypeCreditCardNumber,
- @"emailAddress": UITextContentTypeEmailAddress,
- @"familyName": UITextContentTypeFamilyName,
- @"fullStreetAddress": UITextContentTypeFullStreetAddress,
- @"givenName": UITextContentTypeGivenName,
- @"jobTitle": UITextContentTypeJobTitle,
- @"location": UITextContentTypeLocation,
- @"middleName": UITextContentTypeMiddleName,
- @"name": UITextContentTypeName,
- @"namePrefix": UITextContentTypeNamePrefix,
- @"nameSuffix": UITextContentTypeNameSuffix,
- @"nickname": UITextContentTypeNickname,
- @"organizationName": UITextContentTypeOrganizationName,
- @"postalCode": UITextContentTypePostalCode,
- @"streetAddressLine1": UITextContentTypeStreetAddressLine1,
- @"streetAddressLine2": UITextContentTypeStreetAddressLine2,
- @"sublocality": UITextContentTypeSublocality,
- @"telephoneNumber": UITextContentTypeTelephoneNumber,
- };
- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */
- if (@available(iOS 11.0, tvOS 11.0, *)) {
- NSDictionary<NSString *, NSString *> * iOS11extras = @{@"username": UITextContentTypeUsername,
- @"password": UITextContentTypePassword};
- NSMutableDictionary<NSString *, NSString *> * iOS11baseMap = [contentTypeMap mutableCopy];
- [iOS11baseMap addEntriesFromDictionary:iOS11extras];
- contentTypeMap = [iOS11baseMap copy];
- }
- #endif
- #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 120000 /* __IPHONE_12_0 */
- if (@available(iOS 12.0, tvOS 12.0, *)) {
- NSDictionary<NSString *, NSString *> * iOS12extras = @{@"newPassword": UITextContentTypeNewPassword,
- @"oneTimeCode": UITextContentTypeOneTimeCode};
- NSMutableDictionary<NSString *, NSString *> * iOS12baseMap = [contentTypeMap mutableCopy];
- [iOS12baseMap addEntriesFromDictionary:iOS12extras];
- contentTypeMap = [iOS12baseMap copy];
- }
- #endif
- });
- // Setting textContentType to an empty string will disable any
- // default behaviour, like the autofill bar for password inputs
- self.backedTextInputView.textContentType = contentTypeMap[type] ?: type;
- #endif
- }
- - (void)setPasswordRules:(NSString *)descriptor
- {
- #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0
- if (@available(iOS 12.0, *)) {
- self.backedTextInputView.passwordRules = [UITextInputPasswordRules passwordRulesWithDescriptor:descriptor];
- }
- #endif
- }
- - (UIKeyboardType)keyboardType
- {
- return self.backedTextInputView.keyboardType;
- }
- - (void)setKeyboardType:(UIKeyboardType)keyboardType
- {
- UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
- if (textInputView.keyboardType != keyboardType) {
- textInputView.keyboardType = keyboardType;
- // Without the call to reloadInputViews, the keyboard will not change until the textview field (the first responder) loses and regains focus.
- if (textInputView.isFirstResponder) {
- [textInputView reloadInputViews];
- }
- }
- }
- #pragma mark - RCTBackedTextInputDelegate
- - (BOOL)textInputShouldBeginEditing
- {
- return YES;
- }
- - (void)textInputDidBeginEditing
- {
- if (_clearTextOnFocus) {
- self.backedTextInputView.attributedText = [NSAttributedString new];
- }
- if (_selectTextOnFocus) {
- [self.backedTextInputView selectAll:nil];
- }
- [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
- reactTag:self.reactTag
- text:self.backedTextInputView.attributedText.string
- key:nil
- eventCount:_nativeEventCount];
- }
- - (BOOL)textInputShouldEndEditing
- {
- return YES;
- }
- - (void)textInputDidEndEditing
- {
- [_eventDispatcher sendTextEventWithType:RCTTextEventTypeEnd
- reactTag:self.reactTag
- text:self.backedTextInputView.attributedText.string
- key:nil
- eventCount:_nativeEventCount];
- [_eventDispatcher sendTextEventWithType:RCTTextEventTypeBlur
- reactTag:self.reactTag
- text:self.backedTextInputView.attributedText.string
- key:nil
- eventCount:_nativeEventCount];
- }
- - (BOOL)textInputShouldReturn
- {
- // We send `submit` event here, in `textInputShouldReturn`
- // (not in `textInputDidReturn)`, because of semantic of the event:
- // `onSubmitEditing` is called when "Submit" button
- // (the blue key on onscreen keyboard) did pressed
- // (no connection to any specific "submitting" process).
- [_eventDispatcher sendTextEventWithType:RCTTextEventTypeSubmit
- reactTag:self.reactTag
- text:self.backedTextInputView.attributedText.string
- key:nil
- eventCount:_nativeEventCount];
- return _blurOnSubmit;
- }
- - (void)textInputDidReturn
- {
- // Does nothing.
- }
- - (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
- {
- id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
- if (!backedTextInputView.textWasPasted) {
- [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress
- reactTag:self.reactTag
- text:nil
- key:text
- eventCount:_nativeEventCount];
- }
- if (_maxLength) {
- NSInteger allowedLength = MAX(_maxLength.integerValue - (NSInteger)backedTextInputView.attributedText.string.length + (NSInteger)range.length, 0);
- if (text.length > allowedLength) {
- // If we typed/pasted more than one character, limit the text inputted.
- if (text.length > 1) {
- // Truncate the input string so the result is exactly maxLength
- NSString *limitedString = [text substringToIndex:allowedLength];
- NSMutableAttributedString *newAttributedText = [backedTextInputView.attributedText mutableCopy];
- // Apply text attributes if original input view doesn't have text.
- if (backedTextInputView.attributedText.length == 0) {
- newAttributedText = [[NSMutableAttributedString alloc] initWithString:[self.textAttributes applyTextAttributesToText:limitedString] attributes:self.textAttributes.effectiveTextAttributes];
- } else {
- [newAttributedText replaceCharactersInRange:range withString:limitedString];
- }
- backedTextInputView.attributedText = newAttributedText;
- _predictedText = newAttributedText.string;
- // Collapse selection at end of insert to match normal paste behavior.
- UITextPosition *insertEnd = [backedTextInputView positionFromPosition:backedTextInputView.beginningOfDocument
- offset:(range.location + allowedLength)];
- [backedTextInputView setSelectedTextRange:[backedTextInputView textRangeFromPosition:insertEnd toPosition:insertEnd]
- notifyDelegate:YES];
- [self textInputDidChange];
- }
- return nil; // Rejecting the change.
- }
- }
- NSString *previousText = backedTextInputView.attributedText.string ?: @"";
- if (range.location + range.length > backedTextInputView.attributedText.string.length) {
- _predictedText = backedTextInputView.attributedText.string;
- } else {
- _predictedText = [backedTextInputView.attributedText.string stringByReplacingCharactersInRange:range withString:text];
- }
- if (_onTextInput) {
- _onTextInput(@{
- @"text": text,
- @"previousText": previousText,
- @"range": @{
- @"start": @(range.location),
- @"end": @(range.location + range.length)
- },
- @"eventCount": @(_nativeEventCount),
- });
- }
- return text; // Accepting the change.
- }
- - (void)textInputDidChange
- {
- [self updateLocalData];
- id<RCTBackedTextInputViewProtocol> backedTextInputView = self.backedTextInputView;
- // Detect when `backedTextInputView` updates happened that didn't invoke `shouldChangeTextInRange`
- // (e.g. typing simplified Chinese in pinyin will insert and remove spaces without
- // calling shouldChangeTextInRange). This will cause JS to get out of sync so we
- // update the mismatched range.
- NSRange currentRange;
- NSRange predictionRange;
- if (findMismatch(backedTextInputView.attributedText.string, _predictedText, ¤tRange, &predictionRange)) {
- NSString *replacement = [backedTextInputView.attributedText.string substringWithRange:currentRange];
- [self textInputShouldChangeText:replacement inRange:predictionRange];
- // JS will assume the selection changed based on the location of our shouldChangeTextInRange, so reset it.
- [self textInputDidChangeSelection];
- }
- _nativeEventCount++;
- if (_onChange) {
- _onChange(@{
- @"text": self.attributedText.string,
- @"target": self.reactTag,
- @"eventCount": @(_nativeEventCount),
- });
- }
- }
- - (void)textInputDidChangeSelection
- {
- if (!_onSelectionChange) {
- return;
- }
- RCTTextSelection *selection = self.selection;
- _onSelectionChange(@{
- @"selection": @{
- @"start": @(selection.start),
- @"end": @(selection.end),
- },
- });
- }
- - (void)updateLocalData
- {
- [self enforceTextAttributesIfNeeded];
- [_bridge.uiManager setLocalData:[self.backedTextInputView.attributedText copy]
- forView:self];
- }
- #pragma mark - Layout (in UIKit terms, with all insets)
- - (CGSize)intrinsicContentSize
- {
- CGSize size = self.backedTextInputView.intrinsicContentSize;
- size.width += _reactBorderInsets.left + _reactBorderInsets.right;
- size.height += _reactBorderInsets.top + _reactBorderInsets.bottom;
- // Returning value DOES include border and padding insets.
- return size;
- }
- - (CGSize)sizeThatFits:(CGSize)size
- {
- CGFloat compoundHorizontalBorderInset = _reactBorderInsets.left + _reactBorderInsets.right;
- CGFloat compoundVerticalBorderInset = _reactBorderInsets.top + _reactBorderInsets.bottom;
- size.width -= compoundHorizontalBorderInset;
- size.height -= compoundVerticalBorderInset;
- // Note: `paddingInsets` was already included in `backedTextInputView` size
- // because it was applied as `textContainerInset`.
- CGSize fittingSize = [self.backedTextInputView sizeThatFits:size];
- fittingSize.width += compoundHorizontalBorderInset;
- fittingSize.height += compoundVerticalBorderInset;
- // Returning value DOES include border and padding insets.
- return fittingSize;
- }
- #pragma mark - Accessibility
- - (UIView *)reactAccessibilityElement
- {
- return self.backedTextInputView;
- }
- #pragma mark - Focus Control
- - (void)reactFocus
- {
- [self.backedTextInputView reactFocus];
- }
- - (void)reactBlur
- {
- [self.backedTextInputView reactBlur];
- }
- - (void)didMoveToWindow
- {
- if (self.autoFocus && !_didMoveToWindow) {
- [self.backedTextInputView reactFocus];
- } else {
- [self.backedTextInputView reactFocusIfNeeded];
- }
- _didMoveToWindow = YES;
- }
- #pragma mark - Custom Input Accessory View
- - (void)didSetProps:(NSArray<NSString *> *)changedProps
- {
- if ([changedProps containsObject:@"inputAccessoryViewID"] && self.inputAccessoryViewID) {
- [self setCustomInputAccessoryViewWithNativeID:self.inputAccessoryViewID];
- } else if (!self.inputAccessoryViewID) {
- [self setDefaultInputAccessoryView];
- }
- }
- - (void)setCustomInputAccessoryViewWithNativeID:(NSString *)nativeID
- {
- #if !TARGET_OS_TV
- __weak RCTBaseTextInputView *weakSelf = self;
- [_bridge.uiManager rootViewForReactTag:self.reactTag withCompletion:^(UIView *rootView) {
- RCTBaseTextInputView *strongSelf = weakSelf;
- if (rootView) {
- UIView *accessoryView = [strongSelf->_bridge.uiManager viewForNativeID:nativeID
- withRootTag:rootView.reactTag];
- if (accessoryView && [accessoryView isKindOfClass:[RCTInputAccessoryView class]]) {
- strongSelf.backedTextInputView.inputAccessoryView = ((RCTInputAccessoryView *)accessoryView).inputAccessoryView;
- [strongSelf reloadInputViewsIfNecessary];
- }
- }
- }];
- #endif /* !TARGET_OS_TV */
- }
- - (void)setDefaultInputAccessoryView
- {
- #if !TARGET_OS_TV
- UIView<RCTBackedTextInputViewProtocol> *textInputView = self.backedTextInputView;
- UIKeyboardType keyboardType = textInputView.keyboardType;
- // These keyboard types (all are number pads) don't have a "Done" button by default,
- // so we create an `inputAccessoryView` with this button for them.
- BOOL shouldHaveInputAccesoryView;
- if (@available(iOS 10.0, *)) {
- shouldHaveInputAccesoryView =
- (
- keyboardType == UIKeyboardTypeNumberPad ||
- keyboardType == UIKeyboardTypePhonePad ||
- keyboardType == UIKeyboardTypeDecimalPad ||
- keyboardType == UIKeyboardTypeASCIICapableNumberPad
- ) &&
- textInputView.returnKeyType == UIReturnKeyDone;
- } else {
- shouldHaveInputAccesoryView =
- (
- keyboardType == UIKeyboardTypeNumberPad ||
- keyboardType == UIKeyboardTypePhonePad ||
- keyboardType == UIKeyboardTypeDecimalPad
- ) &&
- textInputView.returnKeyType == UIReturnKeyDone;
- }
- if (_hasInputAccesoryView == shouldHaveInputAccesoryView) {
- return;
- }
- _hasInputAccesoryView = shouldHaveInputAccesoryView;
- if (shouldHaveInputAccesoryView) {
- UIToolbar *toolbarView = [[UIToolbar alloc] init];
- [toolbarView sizeToFit];
- UIBarButtonItem *flexibleSpace =
- [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
- target:nil
- action:nil];
- UIBarButtonItem *doneButton =
- [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone
- target:self
- action:@selector(handleInputAccessoryDoneButton)];
- toolbarView.items = @[flexibleSpace, doneButton];
- textInputView.inputAccessoryView = toolbarView;
- }
- else {
- textInputView.inputAccessoryView = nil;
- }
- [self reloadInputViewsIfNecessary];
- #endif /* !TARGET_OS_TV */
- }
- - (void)reloadInputViewsIfNecessary
- {
- // We have to call `reloadInputViews` for focused text inputs to update an accessory view.
- if (self.backedTextInputView.isFirstResponder) {
- [self.backedTextInputView reloadInputViews];
- }
- }
- - (void)handleInputAccessoryDoneButton
- {
- if ([self textInputShouldReturn]) {
- [self.backedTextInputView endEditing:YES];
- }
- }
- #pragma mark - Helpers
- static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, NSRange *secondRange)
- {
- NSInteger firstMismatch = -1;
- for (NSUInteger ii = 0; ii < MAX(first.length, second.length); ii++) {
- if (ii >= first.length || ii >= second.length || [first characterAtIndex:ii] != [second characterAtIndex:ii]) {
- firstMismatch = ii;
- break;
- }
- }
- if (firstMismatch == -1) {
- return NO;
- }
- NSUInteger ii = second.length;
- NSUInteger lastMismatch = first.length;
- while (ii > firstMismatch && lastMismatch > firstMismatch) {
- if ([first characterAtIndex:(lastMismatch - 1)] != [second characterAtIndex:(ii - 1)]) {
- break;
- }
- ii--;
- lastMismatch--;
- }
- *firstRange = NSMakeRange(firstMismatch, lastMismatch - firstMismatch);
- *secondRange = NSMakeRange(firstMismatch, ii - firstMismatch);
- return YES;
- }
- @end
|