Skip to content

Commit c560531

Browse files
authored
feat: proper iOS first renders measurements (#277)
<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please follow the template so that the reviewers can easily understand what the code changes affect --> # Summary Fixes #238 on iOS. First render on iOS was looking pretty choppy because there was no way to measure the component on the very first render, because it's still not defined at that time. This PR adds a mock text input that gets created as soon as the Shadow Node is created, gets the initial component's props and is used for measurements before the real input is defined. After that the mock gets unmounted (when the real input is finally defined). ## Before vs After Please play the videos at x0.25 speed to notice the choppiness: ### Before: https://github.com/user-attachments/assets/69aa524a-5ec1-4fbc-8220-610d2280eaf4 ### After: https://github.com/user-attachments/assets/8f07e5a1-67e5-4c6d-90df-d0b4f44e0f62 ## Test Plan Add a state that toggles input's visibility, with the default visibility at `false`. Then toggle it and see properly measured component appear with no intermediate states. ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | Android | ✅(already done) |
1 parent c666690 commit c560531

File tree

2 files changed

+47
-17
lines changed

2 files changed

+47
-17
lines changed

ios/internals/EnrichedTextInputViewShadowNode.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class EnrichedTextInputViewShadowNode : public ConcreteViewShadowNode<
3535

3636
private:
3737
int localForceHeightRecalculationCounter_;
38+
static id mockTextInputView_;
39+
void setupMockTextInputView_();
3840
};
3941

4042
} // namespace facebook::react

ios/internals/EnrichedTextInputViewShadowNode.mm

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,34 @@
88
namespace facebook::react {
99

1010
extern const char EnrichedTextInputViewComponentName[] = "EnrichedTextInputView";
11+
id EnrichedTextInputViewShadowNode::mockTextInputView_ = nullptr;
1112

1213
EnrichedTextInputViewShadowNode::EnrichedTextInputViewShadowNode(
1314
const ShadowNodeFragment& fragment,
1415
const ShadowNodeFamily::Shared& family,
1516
ShadowNodeTraits traits
1617
): ConcreteViewShadowNode(fragment, family, traits) {
1718
localForceHeightRecalculationCounter_ = 0;
19+
20+
// mock text input needs to be initialized on the main thread
21+
if([NSThread isMainThread]) {
22+
setupMockTextInputView_();
23+
} else {
24+
dispatch_sync(dispatch_get_main_queue(), ^{
25+
setupMockTextInputView_();
26+
});
27+
}
28+
}
29+
30+
// mock input is used for the first measure calls that need to be done when the real input isn't defined yet
31+
void EnrichedTextInputViewShadowNode::setupMockTextInputView_() {
32+
// it's rendered far away from the viewport
33+
const int veryFarAway = 20000;
34+
const int mockSize = 1000;
35+
mockTextInputView_ = [[EnrichedTextInputView alloc] initWithFrame:(CGRectMake(veryFarAway, veryFarAway, mockSize, mockSize))];
36+
const auto props = this->getProps();
37+
((EnrichedTextInputView *)mockTextInputView_)->blockEmitting = YES;
38+
[mockTextInputView_ updateProps:props oldProps:nullptr];
1839
}
1940

2041
EnrichedTextInputViewShadowNode::EnrichedTextInputViewShadowNode(
@@ -44,6 +65,11 @@
4465
EnrichedTextInputView *typedComponentObject = (EnrichedTextInputView *) componentObject;
4566

4667
if(typedComponentObject != nullptr) {
68+
// remove the mock input on the first render with a defined real input
69+
if(mockTextInputView_ != nullptr) {
70+
mockTextInputView_ = nullptr;
71+
}
72+
4773
__block CGSize estimatedSize;
4874

4975
// synchronously dispatch to main thread if needed
@@ -61,23 +87,25 @@
6187
};
6288
}
6389
} else {
64-
// on the very first call there is no componentView that we can query for the component height
65-
// thus, a little heuristic: just put a height that is exactly height of letter "I" with default apple font and size from props
66-
// in a lot of cases it will be the desired height
67-
// in others, the jump on the second call will at least be smaller
68-
const auto props = this->getProps();
69-
const auto &typedProps = *std::static_pointer_cast<EnrichedTextInputViewProps const>(props);
70-
NSAttributedString *attrStr = [[NSAttributedString alloc] initWithString:@"I" attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:typedProps.fontSize]}];
71-
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
72-
const CGSize &suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(
73-
framesetter,
74-
CFRangeMake(0, 1),
75-
nullptr,
76-
CGSizeMake(layoutConstraints.maximumSize.width, DBL_MAX),
77-
nullptr
78-
);
79-
80-
return {suggestedSize.width, suggestedSize.height};
90+
if(mockTextInputView_ == nullptr) {
91+
return Size();
92+
}
93+
94+
__block CGSize estimatedSize;
95+
96+
// synchronously dispatch to main thread if needed
97+
if([NSThread isMainThread]) {
98+
estimatedSize = [mockTextInputView_ measureSize:layoutConstraints.maximumSize.width];
99+
} else {
100+
dispatch_sync(dispatch_get_main_queue(), ^{
101+
estimatedSize = [mockTextInputView_ measureSize:layoutConstraints.maximumSize.width];
102+
});
103+
}
104+
105+
return {
106+
estimatedSize.width,
107+
MIN(estimatedSize.height, layoutConstraints.maximumSize.height)
108+
};
81109
}
82110

83111
return Size();

0 commit comments

Comments
 (0)