Skip to content

Commit

Permalink
Fabric: The basic implementation of <TextInput> for iOS
Browse files Browse the repository at this point in the history
Summary:
This is the partial implementation of Fabric-compatible <TextInput> component on iOS. All features are supported besides those:
 * `focus()`, `blur()`, `clear()` imperative calls;
 * Controlled TextInput as the whole feature in general;
 * Controlling selection from JavaScript side;
 * `autoFocus` prop;
 * KeyboardAccessoryView.

Changelog: [Internal]

Reviewed By: JoshuaGross

Differential Revision: D17400907

fbshipit-source-id: 0ccd0e0923293e5f504d5fae7b7ba9f048f7d259
  • Loading branch information
shergin authored and facebook-github-bot committed Jan 14, 2020
1 parent 4155796 commit 8219db9
Show file tree
Hide file tree
Showing 3 changed files with 368 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* 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 <UIKit/UIKit.h>

#import <React/RCTViewComponentView.h>

NS_ASSUME_NONNULL_BEGIN

/**
* UIView class for <TextInput> component.
*/
@interface RCTTextInputComponentView : RCTViewComponentView

@end

NS_ASSUME_NONNULL_END
Original file line number Diff line number Diff line change
@@ -0,0 +1,345 @@
/*
* 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 "RCTTextInputComponentView.h"

#import <react/components/iostextinput/TextInputComponentDescriptor.h>
#import <react/graphics/Geometry.h>
#import <react/textlayoutmanager/RCTAttributedTextUtils.h>
#import <react/textlayoutmanager/TextLayoutManager.h>

#import <React/RCTBackedTextInputViewProtocol.h>
#import <React/RCTUITextField.h>
#import <React/RCTUITextView.h>

#import "RCTConversions.h"
#import "RCTTextInputUtils.h"

using namespace facebook::react;

@interface RCTTextInputComponentView () <RCTBackedTextInputDelegate>
@end

@implementation RCTTextInputComponentView {
TextInputShadowNode::ConcreteState::Shared _state;
UIView<RCTBackedTextInputViewProtocol> *_backedTextInputView;
size_t _stateRevision;
}

- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared<TextInputProps const>();
_props = defaultProps;
auto &props = *defaultProps;

_backedTextInputView = props.traits.multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init];
_backedTextInputView.frame = self.bounds;
_backedTextInputView.textInputDelegate = self;
[self addSubview:_backedTextInputView];
}

return self;
}

#pragma mark - RCTComponentViewProtocol

+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider<TextInputComponentDescriptor>();
}

- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
{
auto const &oldTextInputProps = *std::static_pointer_cast<TextInputProps const>(_props);
auto const &newTextInputProps = *std::static_pointer_cast<TextInputProps const>(props);

// Traits:
if (newTextInputProps.traits.multiline != oldTextInputProps.traits.multiline) {
[self _setMultiline:newTextInputProps.traits.multiline];
}

if (newTextInputProps.traits.autocapitalizationType != oldTextInputProps.traits.autocapitalizationType) {
_backedTextInputView.autocapitalizationType =
RCTUITextAutocapitalizationTypeFromAutocapitalizationType(newTextInputProps.traits.autocapitalizationType);
}

if (newTextInputProps.traits.autoCorrect != oldTextInputProps.traits.autoCorrect) {
_backedTextInputView.autocorrectionType =
RCTUITextAutocorrectionTypeFromOptionalBool(newTextInputProps.traits.autoCorrect);
}

if (newTextInputProps.traits.contextMenuHidden != oldTextInputProps.traits.contextMenuHidden) {
_backedTextInputView.contextMenuHidden = newTextInputProps.traits.contextMenuHidden;
}

if (newTextInputProps.traits.editable != oldTextInputProps.traits.editable) {
_backedTextInputView.editable = newTextInputProps.traits.editable;
}

if (newTextInputProps.traits.enablesReturnKeyAutomatically !=
oldTextInputProps.traits.enablesReturnKeyAutomatically) {
_backedTextInputView.enablesReturnKeyAutomatically = newTextInputProps.traits.enablesReturnKeyAutomatically;
}

if (newTextInputProps.traits.keyboardAppearance != oldTextInputProps.traits.keyboardAppearance) {
_backedTextInputView.keyboardAppearance =
RCTUIKeyboardAppearanceFromKeyboardAppearance(newTextInputProps.traits.keyboardAppearance);
}

if (newTextInputProps.traits.spellCheck != oldTextInputProps.traits.spellCheck) {
_backedTextInputView.spellCheckingType =
RCTUITextSpellCheckingTypeFromOptionalBool(newTextInputProps.traits.spellCheck);
}

if (newTextInputProps.traits.caretHidden != oldTextInputProps.traits.caretHidden) {
_backedTextInputView.caretHidden = newTextInputProps.traits.caretHidden;
}

if (newTextInputProps.traits.clearButtonMode != oldTextInputProps.traits.clearButtonMode) {
_backedTextInputView.clearButtonMode =
RCTUITextFieldViewModeFromTextInputAccessoryVisibilityMode(newTextInputProps.traits.clearButtonMode);
}

if (newTextInputProps.traits.scrollEnabled != oldTextInputProps.traits.scrollEnabled) {
_backedTextInputView.scrollEnabled = newTextInputProps.traits.scrollEnabled;
}

if (newTextInputProps.traits.secureTextEntry != oldTextInputProps.traits.secureTextEntry) {
_backedTextInputView.secureTextEntry = newTextInputProps.traits.secureTextEntry;
}

if (newTextInputProps.traits.keyboardType != oldTextInputProps.traits.keyboardType) {
_backedTextInputView.keyboardType = RCTUIKeyboardTypeFromKeyboardType(newTextInputProps.traits.keyboardType);
}

if (newTextInputProps.traits.returnKeyType != oldTextInputProps.traits.returnKeyType) {
_backedTextInputView.returnKeyType = RCTUIReturnKeyTypeFromReturnKeyType(newTextInputProps.traits.returnKeyType);
}

if (newTextInputProps.traits.textContentType != oldTextInputProps.traits.textContentType) {
if (@available(iOS 10.0, *)) {
_backedTextInputView.textContentType = RCTUITextContentTypeFromString(newTextInputProps.traits.textContentType);
}
}

if (newTextInputProps.traits.passwordRules != oldTextInputProps.traits.passwordRules) {
if (@available(iOS 12.0, *)) {
_backedTextInputView.passwordRules =
RCTUITextInputPasswordRulesFromString(newTextInputProps.traits.passwordRules);
}
}

// Traits `blurOnSubmit`, `clearTextOnFocus`, and `selectTextOnFocus` were omitted intentially here
// because they are being checked on-demand.

// Other props:
if (newTextInputProps.placeholder != oldTextInputProps.placeholder) {
_backedTextInputView.placeholder = RCTNSStringFromString(newTextInputProps.placeholder);
}

if (newTextInputProps.textAttributes != oldTextInputProps.textAttributes) {
_backedTextInputView.defaultTextAttributes =
RCTNSTextAttributesFromTextAttributes(newTextInputProps.getEffectiveTextAttributes());
}

if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) {
_backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor);
}

[super updateProps:props oldProps:oldProps];
}

- (void)updateState:(State::Shared const &)state oldState:(State::Shared const &)oldState
{
_state = std::static_pointer_cast<TextInputShadowNode::ConcreteState const>(state);

if (!_state) {
assert(false && "State is `null` for <TextInput> component.");
_backedTextInputView.attributedText = nil;
return;
}

auto data = _state->getData();

if (data.revision != _stateRevision) {
_stateRevision = data.revision;
_backedTextInputView.attributedText = RCTNSAttributedStringFromAttributedStringBox(data.attributedStringBox);
}
}

- (void)updateLayoutMetrics:(LayoutMetrics const &)layoutMetrics
oldLayoutMetrics:(LayoutMetrics const &)oldLayoutMetrics
{
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];

_backedTextInputView.frame =
UIEdgeInsetsInsetRect(self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth));
_backedTextInputView.textContainerInset =
RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.contentInsets - layoutMetrics.borderWidth);
}

- (void)prepareForRecycle
{
[super prepareForRecycle];
_backedTextInputView.attributedText = [[NSAttributedString alloc] init];
_state.reset();
_stateRevision = 0;
}

#pragma mark - RCTComponentViewProtocol

- (void)_setMultiline:(BOOL)multiline
{
[_backedTextInputView removeFromSuperview];
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView =
multiline ? [[RCTUITextView alloc] init] : [[RCTUITextField alloc] init];
backedTextInputView.frame = _backedTextInputView.frame;
RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView);
_backedTextInputView = backedTextInputView;
[self addSubview:_backedTextInputView];
}

#pragma mark - RCTBackedTextInputDelegate

- (BOOL)textInputShouldBeginEditing
{
return YES;
}

- (void)textInputDidBeginEditing
{
auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);

if (props.traits.clearTextOnFocus) {
_backedTextInputView.attributedText = [NSAttributedString new];
[self textInputDidChange];
}

if (props.traits.selectTextOnFocus) {
[_backedTextInputView selectAll:nil];
[self textInputDidChangeSelection];
}

if (_eventEmitter) {
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onFocus([self _textInputMetrics]);
}
}

- (BOOL)textInputShouldEndEditing
{
return YES;
}

- (void)textInputDidEndEditing
{
if (_eventEmitter) {
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onEndEditing([self _textInputMetrics]);
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onBlur([self _textInputMetrics]);
}
}

- (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).

if (_eventEmitter) {
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onSubmitEditing([self _textInputMetrics]);
}

auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
return props.traits.blurOnSubmit;
}

- (void)textInputDidReturn
{
// Does nothing.
}

- (NSString *)textInputShouldChangeText:(NSString *)text inRange:(NSRange)range
{
if (!_backedTextInputView.textWasPasted) {
if (_eventEmitter) {
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onKeyPress([self _textInputMetrics]);
}
}

auto const &props = *std::static_pointer_cast<TextInputProps const>(_props);
if (props.maxLength) {
NSInteger allowedLength = props.maxLength - _backedTextInputView.attributedText.string.length + range.length;

if (allowedLength <= 0) {
return nil;
}

return allowedLength > text.length ? text : [text substringToIndex:allowedLength];
}

return text;
}

- (BOOL)textInputShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
return YES;
}

- (void)textInputDidChange
{
[self _updateState];

if (_eventEmitter) {
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onChange([self _textInputMetrics]);
}
}

- (void)textInputDidChangeSelection
{
if (_eventEmitter) {
std::static_pointer_cast<TextInputEventEmitter const>(_eventEmitter)->onSelectionChange([self _textInputMetrics]);
}
}

#pragma mark - Other

- (TextInputMetrics)_textInputMetrics
{
TextInputMetrics metrics;
metrics.text = RCTStringFromNSString(_backedTextInputView.attributedText.string);
metrics.selectionRange = [self _selectionRange];
return metrics;
}

- (void)_updateState
{
NSAttributedString *attributedString = _backedTextInputView.attributedText;

if (!_state) {
return;
}

auto data = _state->getData();
data.revision++;
_stateRevision = data.revision;
data.attributedStringBox = RCTAttributedStringBoxFromNSAttributedString(attributedString);
_state->updateState(std::move(data), EventPriority::SynchronousUnbatched);
}

- (AttributedString::Range)_selectionRange
{
UITextRange *selectedTextRange = _backedTextInputView.selectedTextRange;
NSInteger start = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
toPosition:selectedTextRange.start];
NSInteger end = [_backedTextInputView offsetFromPosition:_backedTextInputView.beginningOfDocument
toPosition:selectedTextRange.end];
return AttributedString::Range{(int)start, (int)(end - start)};
}

@end
2 changes: 2 additions & 0 deletions React/Fabric/Mounting/RCTComponentViewFactory.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#import "RCTMountingTransactionObserving.h"
#import "RCTParagraphComponentView.h"
#import "RCTRootComponentView.h"
#import "RCTTextInputComponentView.h"
#import "RCTUnimplementedViewComponentView.h"
#import "RCTViewComponentView.h"

Expand All @@ -55,6 +56,7 @@ + (RCTComponentViewFactory *)standardComponentViewFactory
[componentViewFactory registerComponentViewClass:[RCTRootComponentView class]];
[componentViewFactory registerComponentViewClass:[RCTViewComponentView class]];
[componentViewFactory registerComponentViewClass:[RCTParagraphComponentView class]];
[componentViewFactory registerComponentViewClass:[RCTTextInputComponentView class]];

Class<RCTComponentViewProtocol> imageClass = RCTComponentViewClassWithName("Image");
if (imageClass) {
Expand Down

0 comments on commit 8219db9

Please sign in to comment.