Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2064,7 +2064,7 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
FBLazyVector: c12d2108050e27952983d565a232f6f7b1ad5e69
hermes-engine: 57506c1404e8b80c5386de18743e0d02c8d4f681
hermes-engine: c50a496dd5a3974cde1b7d14a944bee1ee64ca0c
RCTDeprecation: 3280799c14232a56e5a44f92981a8ee33bc69fd9
RCTRequired: 9854a51b0f65ccf43ea0b744df4d70fce339db32
RCTSwiftUI: 96986e49a4fdc2c2103929dee2641e1b57edf33d
Expand All @@ -2073,7 +2073,7 @@ SPEC CHECKSUMS:
React: 7ef36630d07638043a134a7dd2ec17e0be10fc3c
React-callinvoker: af4e8fe1d60ab63dd8d74c2a68988064c2848954
React-Core: c0fb1df65eb0ed7a8633841831f05f93c3eb3aff
React-Core-prebuilt: cd92350bf2041dde22a9bc0b8984d9c70d179ca1
React-Core-prebuilt: 25c9d7a48b3bd78017d44e80de5fbb3eb139328d
React-CoreModules: 7dfe7962360355f1547c85ab52e1fc4b57f17127
React-cxxreact: 9e9c7f1710bc58abebf924813b5e825b99adb8e5
React-debug: 38389b86e3570558ec73dd4cbc0cd2f2eec47a51
Expand Down Expand Up @@ -2135,7 +2135,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 625d2f6d9d5ef01acc9dfe2b5385504bbffd2ad0
ReactCodegen: 27937747ddc743fcb66a8dc19e8edf60188d94cc
ReactCommon: cc0e38600f82487c5fe5d29150abb6fa9d981986
ReactNativeDependencies: cebf665879bab2908201494cc5a9760dbdf0a637
ReactNativeDependencies: 8febfeb298c1be3a3699aac801aec086f0580694
ReactNativeEnriched: 790e857a207566a6dc34e7b45480b88bddf79bed
Yoga: 772166513f9cd2d61a6251d0dacbbfaa5b537479

Expand Down
3 changes: 1 addition & 2 deletions ios/EnrichedTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ NS_ASSUME_NONNULL_BEGIN
@public
BOOL blockEmitting;
}
- (CGSize)measureSize:(CGFloat)maxWidth;
- (void)emitOnLinkDetectedEvent:(NSString *)text
url:(NSString *)url
range:(NSRange)range;
Expand All @@ -40,8 +41,6 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range;
- (NSArray<NSNumber *> *)getPresentStyleTypesFrom:(NSArray<NSNumber *> *)types
range:(NSRange)range;
- (CGSize)measureInitialSizeWithMaxWidth:(CGFloat)maxWidth;
- (void)commitSize:(CGSize)size;
@end

NS_ASSUME_NONNULL_END
Expand Down
208 changes: 163 additions & 45 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ - (instancetype)initWithFrame:(CGRect)frame {
_props = defaultProps;
[self setDefaults];
[self setupTextView];
[self addSubview:textView];
[self setupPlaceholderLabel];
self.contentView = textView;
}
return self;
}
Expand Down Expand Up @@ -264,14 +265,31 @@ - (void)setupTextView {
textView.delegate = self;
textView.input = self;
textView.layoutManager.input = self;
textView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
textView.adjustsFontForContentSizeCategory = YES;
[textView addGestureRecognizer:[[TextBlockTapGestureRecognizer alloc]
initWithInput:self
action:@selector(onTextBlockTap:)]];
}

- (void)setupPlaceholderLabel {
_placeholderLabel = [[UILabel alloc] initWithFrame:CGRectZero];
_placeholderLabel.translatesAutoresizingMaskIntoConstraints = NO;
[textView addSubview:_placeholderLabel];
[NSLayoutConstraint activateConstraints:@[
[_placeholderLabel.leadingAnchor
constraintEqualToAnchor:textView.leadingAnchor],
[_placeholderLabel.widthAnchor
constraintEqualToAnchor:textView.widthAnchor],
[_placeholderLabel.topAnchor constraintEqualToAnchor:textView.topAnchor],
[_placeholderLabel.bottomAnchor
constraintEqualToAnchor:textView.bottomAnchor]
]];
_placeholderLabel.lineBreakMode = NSLineBreakByTruncatingTail;
_placeholderLabel.text = @"";
_placeholderLabel.hidden = YES;
_placeholderLabel.adjustsFontForContentSizeCategory = YES;
}

// MARK: - Props

- (void)updateProps:(Props::Shared const &)props
Expand Down Expand Up @@ -717,6 +735,9 @@ - (void)updateProps:(Props::Shared const &)props
[[NSParagraphStyle alloc] init];
textView.typingAttributes = defaultTypingAttributes;
textView.selectedRange = prevSelectedRange;

// update the placeholder as well
[self refreshPlaceholderLabelStyles];
}

// editable
Expand Down Expand Up @@ -744,14 +765,24 @@ - (void)updateProps:(Props::Shared const &)props

// placeholderTextColor
if (newViewProps.placeholderTextColor != oldViewProps.placeholderTextColor) {
textView.placeholderColor =
RCTUIColorFromSharedColor(newViewProps.placeholderTextColor);
// some real color
if (isColorMeaningful(newViewProps.placeholderTextColor)) {
_placeholderColor =
RCTUIColorFromSharedColor(newViewProps.placeholderTextColor);
} else {
_placeholderColor = nullptr;
}
[self refreshPlaceholderLabelStyles];
}

// placeholder
if (newViewProps.placeholder != oldViewProps.placeholder) {
[textView
setPlaceholderText:[NSString fromCppString:newViewProps.placeholder]];
_placeholderLabel.text = [NSString fromCppString:newViewProps.placeholder];
[self refreshPlaceholderLabelStyles];
// additionally show placeholder on first mount if it should be there
if (isFirstMount && textView.text.length == 0) {
[self setPlaceholderLabelShown:YES];
}
}

// mention indicators
Expand Down Expand Up @@ -827,17 +858,75 @@ - (void)updateProps:(Props::Shared const &)props
if (isFirstMount && newViewProps.autoFocus) {
[textView reactFocus];
}
[textView updatePlaceholderVisibility];
}

- (void)updateLayoutMetrics:(const LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const LayoutMetrics &)oldLayoutMetrics {
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
- (void)setPlaceholderLabelShown:(BOOL)shown {
if (shown) {
[self refreshPlaceholderLabelStyles];
_placeholderLabel.hidden = NO;
} else {
_placeholderLabel.hidden = YES;
}
}

- (void)refreshPlaceholderLabelStyles {
NSMutableDictionary *newAttrs = [defaultTypingAttributes mutableCopy];
if (_placeholderColor != nullptr) {
newAttrs[NSForegroundColorAttributeName] = _placeholderColor;
}
NSAttributedString *newAttrStr =
[[NSAttributedString alloc] initWithString:_placeholderLabel.text
attributes:newAttrs];
_placeholderLabel.attributedText = newAttrStr;
}

// MARK: - Measuring and states

- (CGSize)measureSize:(CGFloat)maxWidth {
// copy the the whole attributed string
NSMutableAttributedString *currentStr = [[NSMutableAttributedString alloc]
initWithAttributedString:textView.textStorage];

// edge case: empty input should still be of a height of a single line, so we
// add a mock "I" character
if ([currentStr length] == 0) {
[currentStr
appendAttributedString:[[NSAttributedString alloc]
initWithString:@"I"
attributes:textView.typingAttributes]];
}

// edge case: input with only a zero width space should still be of a height
// of a single line, so we add a mock "I" character
if ([currentStr length] == 1 &&
[[currentStr.string substringWithRange:NSMakeRange(0, 1)]
isEqualToString:@"\u200B"]) {
[currentStr
appendAttributedString:[[NSAttributedString alloc]
initWithString:@"I"
attributes:textView.typingAttributes]];
}

// edge case: trailing newlines aren't counted towards height calculations, so
// we add a mock "I" character
if (currentStr.length > 0) {
unichar lastChar =
[currentStr.string characterAtIndex:currentStr.length - 1];
if ([[NSCharacterSet newlineCharacterSet] characterIsMember:lastChar]) {
[currentStr
appendAttributedString:[[NSAttributedString alloc]
initWithString:@"I"
attributes:defaultTypingAttributes]];
}
}

textView.frame = UIEdgeInsetsInsetRect(
self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth));
textView.textContainerInset = RCTUIEdgeInsetsFromEdgeInsets(
layoutMetrics.contentInsets - layoutMetrics.borderWidth);
CGRect boundingBox =
[currentStr boundingRectWithSize:CGSizeMake(maxWidth, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin |
NSStringDrawingUsesFontLeading
context:nullptr];

return CGSizeMake(maxWidth, ceil(boundingBox.size.height));
}

// make sure the newest state is kept in _state property
Expand All @@ -850,42 +939,18 @@ - (void)updateState:(State::Shared const &)state
// componentView) so we need to run a single height calculation for any
// initial values
if (oldState == nullptr) {
[self commitSize:textView.textContainer.size];
[self tryUpdatingHeight];
}
}

- (void)commitSize:(CGSize)size {
- (void)tryUpdatingHeight {
if (_state == nullptr) {
return;
}

_componentViewHeightUpdateCounter++;
auto selfRef = wrapManagedObjectWeakly(self);
facebook::react::Size newSize{.width = size.width, .height = size.height};
_state->updateState(
facebook::react::EnrichedTextInputViewState(newSize, selfRef));
}

- (CGSize)measureInitialSizeWithMaxWidth:(CGFloat)maxWidth {
NSTextContainer *container = textView.textContainer;
NSLayoutManager *layoutManager = textView.layoutManager;

container.size = CGSizeMake(maxWidth, CGFLOAT_MAX);

[layoutManager ensureLayoutForTextContainer:container];

CGRect used = [layoutManager usedRectForTextContainer:container];
CGFloat height = ceil(used.size.height);

// Empty text fallback
if (textView.textStorage.length == 0) {
UIFont *font =
textView.typingAttributes[NSFontAttributeName] ?: textView.font;
if (font) {
height = ceil(font.lineHeight);
}
}

return CGSizeMake(maxWidth, height);
EnrichedTextInputViewState(_componentViewHeightUpdateCounter, selfRef));
}

// MARK: - Active styles
Expand Down Expand Up @@ -1607,7 +1672,12 @@ - (void)anyTextMayHaveBeenModified {
}

// placholder management
[textView updatePlaceholderVisibility];
if (!_placeholderLabel.hidden && textView.textStorage.string.length > 0) {
[self setPlaceholderLabelShown:NO];
} else if (textView.textStorage.string.length == 0 &&
_placeholderLabel.hidden) {
[self setPlaceholderLabelShown:YES];
}

if (![textView.textStorage.string isEqualToString:_recentInputString]) {
// modified words handling
Expand Down Expand Up @@ -1643,9 +1713,57 @@ - (void)anyTextMayHaveBeenModified {
}
}

// update height on each character change
[self tryUpdatingHeight];
// update active styles as well
[self tryUpdatingActiveStyles];
[self layoutAttachments];
// update drawing - schedule debounced relayout
[self scheduleRelayoutIfNeeded];
}

// Debounced relayout helper - coalesces multiple requests into one per runloop
// tick
- (void)scheduleRelayoutIfNeeded {
// Cancel any previously scheduled invocation to debounce
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(_performRelayout)
object:nil];
// Schedule on next runloop cycle
[self performSelector:@selector(_performRelayout)
withObject:nil
afterDelay:0];
}

- (void)_performRelayout {
if (!textView) {
return;
}

dispatch_async(dispatch_get_main_queue(), ^{
NSRange wholeRange =
NSMakeRange(0, self->textView.textStorage.string.length);
NSRange actualRange = NSMakeRange(0, 0);
[self->textView.layoutManager
invalidateLayoutForCharacterRange:wholeRange
actualCharacterRange:&actualRange];
[self->textView.layoutManager ensureLayoutForCharacterRange:actualRange];
[self->textView.layoutManager
invalidateDisplayForCharacterRange:wholeRange];

// We have to explicitly set contentSize
// That way textView knows if content overflows and if should be scrollable
// We recall measureSize here because value returned from previous
// measureSize may not be up-to date at that point
CGSize measuredSize = [self measureSize:self->textView.frame.size.width];
self->textView.contentSize = measuredSize;
});
}

- (void)didMoveToWindow {
[super didMoveToWindow];
// used to run all lifecycle callbacks
[self anyTextMayHaveBeenModified];
}

// MARK: - UITextView delegate methods
Expand Down Expand Up @@ -1821,7 +1939,7 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
defaultTypingAttributes = newTypingAttrs;
textView.typingAttributes = defaultTypingAttributes;

[textView refreshPlaceholder];
[self refreshPlaceholderLabelStyles];

NSRange prevSelectedRange = textView.selectedRange;

Expand Down
4 changes: 0 additions & 4 deletions ios/inputTextView/InputTextView.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,4 @@

@interface InputTextView : UITextView
@property(nonatomic, weak) id input;
@property(nonatomic, copy, nullable) NSString *placeholderText;
@property(nonatomic, strong, nullable) UIColor *placeholderColor;
- (void)updatePlaceholderVisibility;
- (void)refreshPlaceholder;
@end
Loading
Loading