Skip to content

Commit a613baf

Browse files
YevheniiKotyrlometa-codesync[bot]
authored andcommitted
Leave a view untransformed on iOS when its transform is not finite (#59133)
Summary: A view whose transform holds a `NaN` disappears on iOS, together with the siblings drawn before it. On Android the same view renders untransformed and its siblings are unaffected. I hit it through Reanimated, whose `withTiming` computes a `NaN` for an `undefined` duration, but any transform computed as `NaN` does it. `RCTCATransform3DFromTransformMatrix` copies the resolved matrix into a `CATransform3D` unchecked, so the `NaN` reaches Core Animation. Android's `BaseViewManager` reads each transform value through `sanitizeFloatPropertyValue` first, and on iOS `updateLayoutMetrics` already skips a non-finite frame because a `CALayer` cannot take one. `RCTCATransform3DFromTransformMatrix` now returns the identity for a matrix with a non-finite entry, so `RCTViewComponentView` and the synchronous update in `RCTMountingManager` render the view untransformed, as Android does. ## Changelog: [IOS] [FIXED] - Render a view untransformed when its transform is not finite, instead of hiding it and the siblings drawn before it Pull Request resolved: #59133 Test Plan: - `RCTViewComponentViewTests` gains three cases that set a transform through `updateProps`: a translate with `NaN` and a scale with infinity leave the layer's transform at the identity, and a finite translate is applied. I ran `React-RCTFabric`'s test spec on an iPad (iOS 27.0.1), with the spec enabled in RNTester's Podfile locally: all 22 `RCTViewComponentViewTests` pass with this change, and with `main`'s `RCTConversions.h` the `NaN` and infinity cases fail. The spec's one other failure, `RCTTextInputUtilsTest testCopyBackedTextInputPreservesTintColor`, fails the same way on `main` on that device. - RNTester release builds of `main` and of this change on the same iPad, with a root component that renders a build label, a button, a view whose `translateX` becomes `NaN` after mount, and a second view. On `main` only the second view is drawn: the label, the button and the `NaN` view are gone. With this change all four are drawn and the `NaN` view sits untransformed. The before and after are in a comment below. - clang-format 21.1.2 passes on both files. Reviewed By: cipolleschi Differential Revision: D126289250 Pulled By: vzaidman fbshipit-source-id: d94e1aa110ab2d03ab66923dc435c5ef990df557
1 parent fcd141c commit a613baf

2 files changed

Lines changed: 41 additions & 0 deletions

File tree

‎packages/react-native/React/Fabric/RCTConversions.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
#import <cmath>
9+
810
#import <UIKit/UIKit.h>
911

1012
#import <React/Graphics.h>
@@ -152,6 +154,12 @@ inline UIAccessibilityTraits RCTUIAccessibilityTraitsFromAccessibilityTraits(
152154

153155
inline CATransform3D RCTCATransform3DFromTransformMatrix(const facebook::react::Transform &transformMatrix)
154156
{
157+
// Core Animation needs a finite transform: a NaN in one hides the layer and can hide its siblings.
158+
for (const auto value : transformMatrix.matrix) {
159+
if (!std::isfinite(value)) {
160+
return CATransform3DIdentity;
161+
}
162+
}
155163
return {
156164
(CGFloat)transformMatrix.matrix[0],
157165
(CGFloat)transformMatrix.matrix[1],

‎packages/react-native/React/Tests/Mounting/RCTViewComponentViewTests.mm‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,39 @@ - (void)testHitTestAfterScaleTransitionedToZeroReturnsNil
204204
XCTAssertNil([view hitTest:CGPointMake(50, 50) withEvent:nil]);
205205
}
206206

207+
#pragma mark - non-finite transforms
208+
209+
static RCTViewComponentView *makeViewWithTransform(const Transform &transform)
210+
{
211+
RCTViewComponentView *view = [RCTViewComponentView new];
212+
auto props = std::make_shared<ViewProps>();
213+
props->transform = transform;
214+
[view updateProps:props oldProps:ViewShadowNode::defaultSharedProps()];
215+
return view;
216+
}
217+
218+
- (void)testFiniteTransformIsApplied
219+
{
220+
RCTViewComponentView *view = makeViewWithTransform(Transform::Translate(5, 10, 0));
221+
222+
XCTAssertEqual(view.layer.transform.m41, 5.0);
223+
XCTAssertEqual(view.layer.transform.m42, 10.0);
224+
}
225+
226+
- (void)testTransformWithNaNIsNotApplied
227+
{
228+
RCTViewComponentView *view = makeViewWithTransform(Transform::Translate(NAN, 10, 0));
229+
230+
XCTAssertTrue(CATransform3DIsIdentity(view.layer.transform));
231+
}
232+
233+
- (void)testTransformWithInfinityIsNotApplied
234+
{
235+
RCTViewComponentView *view = makeViewWithTransform(Transform::Scale(INFINITY, 1, 1));
236+
237+
XCTAssertTrue(CATransform3DIsIdentity(view.layer.transform));
238+
}
239+
207240
#pragma mark - Full Keyboard Access focusability
208241

209242
static RCTViewComponentView *makeViewWithRole(bool accessible, const std::string &accessibilityRole)

0 commit comments

Comments
 (0)