From b53d76efb7c5e7239061267a28d69b51fb068dfe Mon Sep 17 00:00:00 2001 From: Valentin Shergin Date: Mon, 20 Mar 2017 00:00:23 -0700 Subject: [PATCH] Better TextInput: RCTUITextView was decoupled in separate file and now handles placeholder feature Reviewed By: mmmulani Differential Revision: D4663151 fbshipit-source-id: ce57ca4bebf4676df2ae5e586a1b175ec2aac760 --- .../UIExplorer/js/TextInputExample.ios.js | 2 +- .../Text/RCTText.xcodeproj/project.pbxproj | 8 + Libraries/Text/RCTTextView.h | 1 + Libraries/Text/RCTTextView.m | 291 ++++++------------ Libraries/Text/RCTUITextView.h | 28 ++ Libraries/Text/RCTUITextView.m | 189 ++++++++++++ React/Base/RCTBatchedBridge.m | 1 + 7 files changed, 323 insertions(+), 197 deletions(-) create mode 100644 Libraries/Text/RCTUITextView.h create mode 100644 Libraries/Text/RCTUITextView.m diff --git a/Examples/UIExplorer/js/TextInputExample.ios.js b/Examples/UIExplorer/js/TextInputExample.ios.js index 960066872..89f8ccaff 100644 --- a/Examples/UIExplorer/js/TextInputExample.ios.js +++ b/Examples/UIExplorer/js/TextInputExample.ios.js @@ -779,7 +779,7 @@ exports.examples = [ RCTTextUpdateLagWarningThreshold) { + RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); + } +} + +- (NSString *)text +{ + return _textView.text; +} + +- (void)setText:(NSString *)text +{ + NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; + if (eventLag == 0 && ![text isEqualToString:_textView.text]) { + UITextRange *selection = _textView.selectedTextRange; + NSInteger oldTextLength = _textView.text.length; + + _predictedText = text; + _textView.text = text; + + if (selection.empty) { + // maintain cursor position relative to the end of the old text + NSInteger start = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:selection.start]; + NSInteger offsetFromEnd = oldTextLength - start; + NSInteger newOffset = text.length - offsetFromEnd; + UITextPosition *position = [_textView positionFromPosition:_textView.beginningOfDocument offset:newOffset]; + _textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position]; + } + + [self invalidateContentSize]; + } else if (eventLag > RCTTextUpdateLagWarningThreshold) { + RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); + } +} + +- (NSString *)placeholder +{ + return _textView.placeholderText; +} + +- (void)setPlaceholder:(NSString *)placeholder +{ + _textView.placeholderText = placeholder; +} + +- (UIColor *)placeholderTextColor +{ + return _textView.placeholderTextColor; +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ + _textView.placeholderTextColor = placeholderTextColor; +} + +- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType +{ + _textView.autocorrectionType = autocorrectionType; +} + +- (UITextAutocorrectionType)autocorrectionType +{ + return _textView.autocorrectionType; +} + +- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType +{ + _textView.spellCheckingType = spellCheckingType; +} + +- (UITextSpellCheckingType)spellCheckingType +{ + return _textView.spellCheckingType; +} + #pragma mark - UITextViewDelegate - (BOOL)textView:(RCTUITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { - if (textView.textWasPasted) { - textView.textWasPasted = NO; - } else { + if (!textView.textWasPasted) { [_eventDispatcher sendTextEventWithType:RCTTextEventTypeKeyPress reactTag:self.reactTag text:nil @@ -425,86 +417,6 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) } } -- (NSString *)text -{ - return _textView.text; -} - -- (void)setSelection:(RCTTextSelection *)selection -{ - if (!selection) { - return; - } - - UITextRange *currentSelection = _textView.selectedTextRange; - UITextPosition *start = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.start]; - UITextPosition *end = [_textView positionFromPosition:_textView.beginningOfDocument offset:selection.end]; - UITextRange *selectedTextRange = [_textView textRangeFromPosition:start toPosition:end]; - - NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; - if (eventLag == 0 && ![currentSelection isEqual:selectedTextRange]) { - _previousSelectionRange = selectedTextRange; - _textView.selectedTextRange = selectedTextRange; - } else if (eventLag > RCTTextUpdateLagWarningThreshold) { - RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); - } -} - -- (void)setText:(NSString *)text -{ - NSInteger eventLag = _nativeEventCount - _mostRecentEventCount; - if (eventLag == 0 && ![text isEqualToString:_textView.text]) { - UITextRange *selection = _textView.selectedTextRange; - NSInteger oldTextLength = _textView.text.length; - - _predictedText = text; - _textView.text = text; - - if (selection.empty) { - // maintain cursor position relative to the end of the old text - NSInteger start = [_textView offsetFromPosition:_textView.beginningOfDocument toPosition:selection.start]; - NSInteger offsetFromEnd = oldTextLength - start; - NSInteger newOffset = text.length - offsetFromEnd; - UITextPosition *position = [_textView positionFromPosition:_textView.beginningOfDocument offset:newOffset]; - _textView.selectedTextRange = [_textView textRangeFromPosition:position toPosition:position]; - } - - [self updatePlaceholderVisibility]; - [self invalidateContentSize]; - } else if (eventLag > RCTTextUpdateLagWarningThreshold) { - RCTLogWarn(@"Native TextInput(%@) is %zd events ahead of JS - try to make your JS faster.", self.text, eventLag); - } -} - -- (void)updatePlaceholderVisibility -{ - if (_textView.text.length > 0) { - [_placeholderView setHidden:YES]; - } else { - [_placeholderView setHidden:NO]; - } -} - -- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType -{ - _textView.autocorrectionType = autocorrectionType; -} - -- (UITextAutocorrectionType)autocorrectionType -{ - return _textView.autocorrectionType; -} - -- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType -{ - _textView.spellCheckingType = spellCheckingType; -} - -- (UITextSpellCheckingType)spellCheckingType -{ - return _textView.spellCheckingType; -} - - (BOOL)textViewShouldBeginEditing:(UITextView *)textView { if (_selectTextOnFocus) { @@ -519,7 +431,6 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string) { if (_clearTextOnFocus) { _textView.text = @""; - [self updatePlaceholderVisibility]; } [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus @@ -560,7 +471,6 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, - (void)textViewDidChange:(UITextView *)textView { - [self updatePlaceholderVisibility]; [self invalidateContentSize]; // Detect when textView updates happend that didn't invoke `shouldChangeTextInRange` @@ -580,6 +490,7 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, _nativeUpdatesInFlight = NO; _nativeEventCount++; + // TODO: t16435709 This part will be removed soon. if (!self.reactTag || !_onChange) { return; } @@ -718,18 +629,6 @@ static BOOL findMismatch(NSString *first, NSString *second, NSRange *firstRange, [self invalidateContentSize]; } -#pragma mark - Default values - -- (UIFont *)defaultPlaceholderFont -{ - return [UIFont systemFontOfSize:17]; -} - -- (UIColor *)defaultPlaceholderTextColor -{ - return [UIColor colorWithRed:0.0/255.0 green:0.0/255.0 blue:0.098/255.0 alpha:0.22]; -} - #pragma mark - UIScrollViewDelegate - (void)scrollViewDidScroll:(UIScrollView *)scrollView diff --git a/Libraries/Text/RCTUITextView.h b/Libraries/Text/RCTUITextView.h new file mode 100644 index 000000000..762c1c2ad --- /dev/null +++ b/Libraries/Text/RCTUITextView.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/* + * Just regular UITextView... but much better! + */ +@interface RCTUITextView : UITextView + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(nullable NSTextContainer *)textContainer NS_UNAVAILABLE; +- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; + +@property (nonatomic, assign, readonly) BOOL textWasPasted; +@property (nonatomic, copy, nullable) NSString *placeholderText; +@property (nonatomic, assign, nullable) UIColor *placeholderTextColor; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Libraries/Text/RCTUITextView.m b/Libraries/Text/RCTUITextView.m new file mode 100644 index 000000000..24302f823 --- /dev/null +++ b/Libraries/Text/RCTUITextView.m @@ -0,0 +1,189 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTUITextView.h" + +@implementation RCTUITextView +{ + BOOL _jsRequestingFirstResponder; + UILabel *_placeholderView; + UITextView *_detachedTextView; +} + +static UIFont *defaultPlaceholderFont() +{ + return [UIFont systemFontOfSize:17]; +} + +static UIColor *defaultPlaceholderTextColor() +{ + // Default placeholder color from UITextField. + return [UIColor colorWithRed:0 green:0 blue:0.0980392 alpha:0.22]; +} + +- (instancetype)initWithFrame:(CGRect)frame +{ + if (self = [super initWithFrame:frame]) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(textDidChange) + name:UITextViewTextDidChangeNotification + object:self]; + + _placeholderView = [[UILabel alloc] initWithFrame:self.bounds]; + _placeholderView.hidden = YES; + _placeholderView.isAccessibilityElement = NO; + _placeholderView.numberOfLines = 0; + [self addSubview:_placeholderView]; + } + + return self; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Properties + +- (void)setPlaceholderText:(NSString *)placeholderText +{ + _placeholderText = placeholderText; + [self invalidatePlaceholder]; +} + +- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor +{ + _placeholderTextColor = placeholderTextColor; + [self invalidatePlaceholder]; +} + + +- (void)textDidChange +{ + _textWasPasted = NO; + [self invalidatePlaceholder]; +} + +#pragma mark - UIResponder + +- (void)reactWillMakeFirstResponder +{ + _jsRequestingFirstResponder = YES; +} + +- (BOOL)canBecomeFirstResponder +{ + return _jsRequestingFirstResponder; +} + +- (void)reactDidMakeFirstResponder +{ + _jsRequestingFirstResponder = NO; +} + +- (void)didMoveToWindow +{ + if (_jsRequestingFirstResponder) { + [self becomeFirstResponder]; + [self reactDidMakeFirstResponder]; + } +} + +#pragma mark - Overrides + +- (void)setFont:(UIFont *)font +{ + [super setFont:font]; + [self invalidatePlaceholder]; +} + +- (void)setText:(NSString *)text +{ + [super setText:text]; + [self textDidChange]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + [super setAttributedText:attributedText]; + [self textDidChange]; +} + +- (void)paste:(id)sender +{ + [super paste:sender]; + _textWasPasted = YES; +} + +- (void)setContentOffset:(CGPoint)contentOffset animated:(__unused BOOL)animated +{ + // Turning off scroll animation. + // This fixes the problem also known as "flaky scrolling". + [super setContentOffset:contentOffset animated:NO]; +} + +#pragma mark - Layout + +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, self.textContainerInset); + CGFloat placeholderHeight = [_placeholderView sizeThatFits:textFrame.size].height; + textFrame.size.height = MIN(placeholderHeight, textFrame.size.height); + _placeholderView.frame = textFrame; +} + +- (CGSize)sizeThatFits:(CGSize)size +{ + // UITextView on iOS 8 has a bug that automatically scrolls to the top + // when calling `sizeThatFits:`. Use a copy so that self is not screwed up. + static BOOL useCustomImplementation = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + useCustomImplementation = ![[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){9,0,0}]; + }); + + if (!useCustomImplementation) { + return [super sizeThatFits:size]; + } + + if (!_detachedTextView) { + _detachedTextView = [UITextView new]; + } + + _detachedTextView.attributedText = self.attributedText; + _detachedTextView.font = self.font; + _detachedTextView.textContainerInset = self.textContainerInset; + + return [_detachedTextView sizeThatFits:size]; +} + +#pragma mark - Placeholder + +- (void)invalidatePlaceholder +{ + BOOL wasVisible = !_placeholderView.isHidden; + BOOL isVisible = _placeholderText.length != 0 && self.text.length == 0; + + if (wasVisible != isVisible) { + _placeholderView.hidden = !isVisible; + } + + if (isVisible) { + _placeholderView.font = self.font ?: defaultPlaceholderFont(); + _placeholderView.textColor = _placeholderTextColor ?: defaultPlaceholderTextColor(); + _placeholderView.textAlignment = self.textAlignment; + _placeholderView.text = _placeholderText; + [self setNeedsLayout]; + } +} + +@end diff --git a/React/Base/RCTBatchedBridge.m b/React/Base/RCTBatchedBridge.m index 4c065d0d6..480df01a9 100644 --- a/React/Base/RCTBatchedBridge.m +++ b/React/Base/RCTBatchedBridge.m @@ -10,6 +10,7 @@ #import #import "RCTAssert.h" + #import "RCTBridge+Private.h" #import "RCTBridge.h" #import "RCTBridgeMethod.h"