diff --git a/ios/internals/EnrichedTextInputViewShadowNode.h b/ios/internals/EnrichedTextInputViewShadowNode.h index 1e3c5ffba..c70bf03ea 100644 --- a/ios/internals/EnrichedTextInputViewShadowNode.h +++ b/ios/internals/EnrichedTextInputViewShadowNode.h @@ -35,6 +35,8 @@ class EnrichedTextInputViewShadowNode : public ConcreteViewShadowNode< private: int localForceHeightRecalculationCounter_; + static id mockTextInputView_; + void setupMockTextInputView_(); }; } // namespace facebook::react diff --git a/ios/internals/EnrichedTextInputViewShadowNode.mm b/ios/internals/EnrichedTextInputViewShadowNode.mm index 647d5f157..86d9e43e9 100644 --- a/ios/internals/EnrichedTextInputViewShadowNode.mm +++ b/ios/internals/EnrichedTextInputViewShadowNode.mm @@ -8,6 +8,7 @@ namespace facebook::react { extern const char EnrichedTextInputViewComponentName[] = "EnrichedTextInputView"; +id EnrichedTextInputViewShadowNode::mockTextInputView_ = nullptr; EnrichedTextInputViewShadowNode::EnrichedTextInputViewShadowNode( const ShadowNodeFragment& fragment, @@ -15,6 +16,26 @@ ShadowNodeTraits traits ): ConcreteViewShadowNode(fragment, family, traits) { localForceHeightRecalculationCounter_ = 0; + + // mock text input needs to be initialized on the main thread + if([NSThread isMainThread]) { + setupMockTextInputView_(); + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + setupMockTextInputView_(); + }); + } +} + +// mock input is used for the first measure calls that need to be done when the real input isn't defined yet +void EnrichedTextInputViewShadowNode::setupMockTextInputView_() { + // it's rendered far away from the viewport + const int veryFarAway = 20000; + const int mockSize = 1000; + mockTextInputView_ = [[EnrichedTextInputView alloc] initWithFrame:(CGRectMake(veryFarAway, veryFarAway, mockSize, mockSize))]; + const auto props = this->getProps(); + ((EnrichedTextInputView *)mockTextInputView_)->blockEmitting = YES; + [mockTextInputView_ updateProps:props oldProps:nullptr]; } EnrichedTextInputViewShadowNode::EnrichedTextInputViewShadowNode( @@ -44,6 +65,11 @@ EnrichedTextInputView *typedComponentObject = (EnrichedTextInputView *) componentObject; if(typedComponentObject != nullptr) { + // remove the mock input on the first render with a defined real input + if(mockTextInputView_ != nullptr) { + mockTextInputView_ = nullptr; + } + __block CGSize estimatedSize; // synchronously dispatch to main thread if needed @@ -61,23 +87,25 @@ }; } } else { - // on the very first call there is no componentView that we can query for the component height - // thus, a little heuristic: just put a height that is exactly height of letter "I" with default apple font and size from props - // in a lot of cases it will be the desired height - // in others, the jump on the second call will at least be smaller - const auto props = this->getProps(); - const auto &typedProps = *std::static_pointer_cast(props); - NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@"I" attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:typedProps.fontSize]}]; - CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr); - const CGSize &suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints( - framesetter, - CFRangeMake(0, 1), - nullptr, - CGSizeMake(layoutConstraints.maximumSize.width, DBL_MAX), - nullptr - ); - - return {suggestedSize.width, suggestedSize.height}; + if(mockTextInputView_ == nullptr) { + return Size(); + } + + __block CGSize estimatedSize; + + // synchronously dispatch to main thread if needed + if([NSThread isMainThread]) { + estimatedSize = [mockTextInputView_ measureSize:layoutConstraints.maximumSize.width]; + } else { + dispatch_sync(dispatch_get_main_queue(), ^{ + estimatedSize = [mockTextInputView_ measureSize:layoutConstraints.maximumSize.width]; + }); + } + + return { + estimatedSize.width, + MIN(estimatedSize.height, layoutConstraints.maximumSize.height) + }; } return Size();