Better TextInput: RCTUITextView was decoupled in separate file and now handles placeholder feature

Reviewed By: mmmulani

Differential Revision: D4663151

fbshipit-source-id: ce57ca4bebf4676df2ae5e586a1b175ec2aac760
This commit is contained in:
Valentin Shergin
2017-03-20 00:00:23 -07:00
committed by Facebook Github Bot
parent 26e2c08544
commit b53d76efb7
7 changed files with 323 additions and 197 deletions

View File

@@ -27,6 +27,8 @@
58B511D01A9E6C5C00147676 /* RCTShadowText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CB1A9E6C5C00147676 /* RCTShadowText.m */; };
58B511D11A9E6C5C00147676 /* RCTTextManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B511CD1A9E6C5C00147676 /* RCTTextManager.m */; };
58B512161A9E6EFF00147676 /* RCTText.m in Sources */ = {isa = PBXBuildFile; fileRef = 58B512141A9E6EFF00147676 /* RCTText.m */; };
59B125C91E6E4E15004E2A67 /* RCTUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59B125C81E6E4E15004E2A67 /* RCTUITextView.m */; };
59B125CA1E6E4E15004E2A67 /* RCTUITextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59B125C81E6E4E15004E2A67 /* RCTUITextView.m */; };
59F60E911E661BDD0081153B /* RCTShadowTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 59F60E8E1E661BDD0081153B /* RCTShadowTextField.m */; };
59F60E921E661BDD0081153B /* RCTShadowTextField.m in Sources */ = {isa = PBXBuildFile; fileRef = 59F60E8E1E661BDD0081153B /* RCTShadowTextField.m */; };
59F60E931E661BDD0081153B /* RCTShadowTextView.m in Sources */ = {isa = PBXBuildFile; fileRef = 59F60E901E661BDD0081153B /* RCTShadowTextView.m */; };
@@ -58,6 +60,8 @@
58B511CD1A9E6C5C00147676 /* RCTTextManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTextManager.m; sourceTree = "<group>"; };
58B512141A9E6EFF00147676 /* RCTText.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTText.m; sourceTree = "<group>"; };
58B512151A9E6EFF00147676 /* RCTText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTText.h; sourceTree = "<group>"; };
59B125C71E6E4E15004E2A67 /* RCTUITextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTUITextView.h; sourceTree = "<group>"; };
59B125C81E6E4E15004E2A67 /* RCTUITextView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTUITextView.m; sourceTree = "<group>"; };
59F60E8D1E661BDD0081153B /* RCTShadowTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowTextField.h; sourceTree = "<group>"; };
59F60E8E1E661BDD0081153B /* RCTShadowTextField.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTShadowTextField.m; sourceTree = "<group>"; };
59F60E8F1E661BDD0081153B /* RCTShadowTextView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTShadowTextView.h; sourceTree = "<group>"; };
@@ -97,6 +101,8 @@
131B6ABD1AF0CD0600FFC3E0 /* RCTTextView.m */,
131B6ABE1AF0CD0600FFC3E0 /* RCTTextViewManager.h */,
131B6ABF1AF0CD0600FFC3E0 /* RCTTextViewManager.m */,
59B125C71E6E4E15004E2A67 /* RCTUITextView.h */,
59B125C81E6E4E15004E2A67 /* RCTUITextView.m */,
);
indentWidth = 2;
sourceTree = "<group>";
@@ -194,6 +200,7 @@
2D3B5F341D9B103100451313 /* RCTRawTextManager.m in Sources */,
59F60E921E661BDD0081153B /* RCTShadowTextField.m in Sources */,
AF3225FA1DE5574F00D3E7E7 /* RCTConvert+Text.m in Sources */,
59B125CA1E6E4E15004E2A67 /* RCTUITextView.m in Sources */,
2D3B5F3C1D9B106F00451313 /* RCTTextViewManager.m in Sources */,
59F60E941E661BDD0081153B /* RCTShadowTextView.m in Sources */,
2D3B5F331D9B102D00451313 /* RCTTextSelection.m in Sources */,
@@ -214,6 +221,7 @@
1362F1011B4D51F400E06D8C /* RCTTextFieldManager.m in Sources */,
59F60E911E661BDD0081153B /* RCTShadowTextField.m in Sources */,
AF3225F91DE5574F00D3E7E7 /* RCTConvert+Text.m in Sources */,
59B125C91E6E4E15004E2A67 /* RCTUITextView.m in Sources */,
131B6AC11AF0CD0600FFC3E0 /* RCTTextViewManager.m in Sources */,
59F60E931E661BDD0081153B /* RCTShadowTextView.m in Sources */,
58B511CF1A9E6C5C00147676 /* RCTShadowRawText.m in Sources */,

View File

@@ -25,6 +25,7 @@
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIColor *placeholderTextColor;
@property (nonatomic, copy) NSString *placeholder;
@property (nonatomic, strong) UIFont *font;
@property (nonatomic, assign) NSInteger mostRecentEventCount;
@property (nonatomic, strong) NSNumber *maxLength;

View File

@@ -18,64 +18,14 @@
#import "RCTShadowText.h"
#import "RCTText.h"
#import "RCTTextSelection.h"
@interface RCTUITextView : UITextView
@property (nonatomic, assign) BOOL textWasPasted;
@end
@implementation RCTUITextView
{
BOOL _jsRequestingFirstResponder;
}
- (void)paste:(id)sender
{
_textWasPasted = YES;
[super paste:sender];
}
- (void)reactWillMakeFirstResponder
{
_jsRequestingFirstResponder = YES;
}
- (BOOL)canBecomeFirstResponder
{
return _jsRequestingFirstResponder;
}
- (void)reactDidMakeFirstResponder
{
_jsRequestingFirstResponder = NO;
}
- (void)didMoveToWindow
{
if (_jsRequestingFirstResponder) {
[self becomeFirstResponder];
[self reactDidMakeFirstResponder];
}
}
- (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];
}
@end
#import "RCTUITextView.h"
@implementation RCTTextView
{
RCTBridge *_bridge;
RCTEventDispatcher *_eventDispatcher;
NSString *_placeholder;
UITextView *_placeholderView;
UITextView *_textView;
RCTUITextView *_textView;
RCTText *_richTextView;
NSAttributedString *_pendingAttributedText;
@@ -99,7 +49,6 @@
_contentInset = UIEdgeInsetsZero;
_bridge = bridge;
_eventDispatcher = bridge.eventDispatcher;
_placeholderTextColor = [self defaultPlaceholderTextColor];
_blurOnSubmit = NO;
_textView = [[RCTUITextView alloc] initWithFrame:self.bounds];
@@ -241,39 +190,12 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
[_textView layoutIfNeeded];
[self updatePlaceholderVisibility];
[self invalidateContentSize];
_blockTextShouldChange = NO;
}
- (void)updatePlaceholder
{
[_placeholderView removeFromSuperview];
_placeholderView = nil;
if (_placeholder) {
_placeholderView = [[UITextView alloc] initWithFrame:_textView.frame];
_placeholderView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_placeholderView.textContainer.lineFragmentPadding = 0;
_placeholderView.userInteractionEnabled = NO;
_placeholderView.backgroundColor = [UIColor clearColor];
_placeholderView.scrollEnabled = NO;
#if !TARGET_OS_TV
_placeholderView.editable = NO;
_placeholderView.scrollsToTop = NO;
#endif
_placeholderView.attributedText =
[[NSAttributedString alloc] initWithString:_placeholder attributes:@{
NSFontAttributeName : (_textView.font ? _textView.font : [self defaultPlaceholderFont]),
NSForegroundColorAttributeName : _placeholderTextColor
}];
_placeholderView.textAlignment = _textView.textAlignment;
[self insertSubview:_placeholderView belowSubview:_textView];
[self updatePlaceholderVisibility];
}
}
#pragma mark - Properties
- (UIFont *)font
{
@@ -283,40 +205,110 @@ static NSAttributedString *removeReactTagFromString(NSAttributedString *string)
- (void)setFont:(UIFont *)font
{
_textView.font = font;
[self updatePlaceholder];
}
- (void)setPlaceholder:(NSString *)placeholder
{
_placeholder = placeholder;
[self updatePlaceholder];
}
- (void)setPlaceholderTextColor:(UIColor *)placeholderTextColor
{
if (placeholderTextColor) {
_placeholderTextColor = placeholderTextColor;
} else {
_placeholderTextColor = [self defaultPlaceholderTextColor];
}
[self updatePlaceholder];
}
- (void)setContentInset:(UIEdgeInsets)contentInset
{
_contentInset = contentInset;
_textView.textContainerInset = contentInset;
_placeholderView.textContainerInset = contentInset;
[self setNeedsLayout];
}
- (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);
}
}
- (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

View File

@@ -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 <UIKit/UIKit.h>
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

View File

@@ -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