Skip to content
Merged
3 changes: 2 additions & 1 deletion ios/EnrichedTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ NS_ASSUME_NONNULL_BEGIN
@public
BOOL blockEmitting;
}
- (CGSize)measureSize:(CGFloat)maxWidth;
- (void)emitOnLinkDetectedEvent:(NSString *)text
url:(NSString *)url
range:(NSRange)range;
Expand All @@ -40,6 +39,8 @@ 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: 45 additions & 163 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ - (instancetype)initWithFrame:(CGRect)frame {
_props = defaultProps;
[self setDefaults];
[self setupTextView];
[self setupPlaceholderLabel];
self.contentView = textView;
[self addSubview:textView];
}
return self;
}
Expand Down Expand Up @@ -249,28 +248,11 @@ - (void)setupTextView {
textView.delegate = self;
textView.input = self;
textView.layoutManager.input = self;
textView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
textView.adjustsFontForContentSizeCategory = YES;
}

- (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 @@ -685,9 +667,6 @@ - (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 @@ -715,24 +694,14 @@ - (void)updateProps:(Props::Shared const &)props

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

// placeholder
if (newViewProps.placeholder != oldViewProps.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];
}
[textView
setPlaceholderText:[NSString fromCppString:newViewProps.placeholder]];
}

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

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

- (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]];
}
}

CGRect boundingBox =
[currentStr boundingRectWithSize:CGSizeMake(maxWidth, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin |
NSStringDrawingUsesFontLeading
context:nullptr];

return CGSizeMake(maxWidth, ceil(boundingBox.size.height));
textView.frame = UIEdgeInsetsInsetRect(
self.bounds, RCTUIEdgeInsetsFromEdgeInsets(layoutMetrics.borderWidth));
textView.textContainerInset = RCTUIEdgeInsetsFromEdgeInsets(
layoutMetrics.contentInsets - layoutMetrics.borderWidth);
}

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

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

auto selfRef = wrapManagedObjectWeakly(self);
facebook::react::Size newSize{.width = size.width, .height = size.height};
_state->updateState(
EnrichedTextInputViewState(_componentViewHeightUpdateCounter, selfRef));
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);
}

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

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

if (![textView.textStorage.string isEqualToString:_recentInputString]) {
// modified words handling
Expand Down Expand Up @@ -1658,56 +1588,8 @@ - (void)anyTextMayHaveBeenModified {
}
}

// update height on each character change
[self tryUpdatingHeight];
// update active styles as well
[self tryUpdatingActiveStyles];
// 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 @@ -1874,7 +1756,7 @@ - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
defaultTypingAttributes = newTypingAttrs;
textView.typingAttributes = defaultTypingAttributes;

[self refreshPlaceholderLabelStyles];
[textView refreshPlaceholder];

NSRange prevSelectedRange = textView.selectedRange;

Expand Down
8 changes: 4 additions & 4 deletions ios/extensions/LayoutManagerExtension.mm
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,11 @@ - (void)drawLists:(EnrichedTextInputView *)typedInput
CGFloat bulletSize =
[typedInput->config
unorderedListBulletSize];
CGFloat bulletX = usedRect.origin.x -
gapWidth -
bulletSize / 2;
CGFloat bulletX =
origin.x + usedRect.origin.x -
bulletSize / 2 - gapWidth;
CGFloat centerY =
CGRectGetMidY(usedRect);
CGRectGetMidY(usedRect) + origin.y;

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

@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