Skip to content

iOS Layout Issues in New Architecture: Unexpected view height changes on re-render #2802

@Jpoliachik

Description

@Jpoliachik

Description

We've seen several layout issues after switching to New Architecture as documented and reproduced here https://github.com/Jpoliachik/NewArchNavLayoutIssues
I spent several days digging into these to try and find the root cause, here is what I found for iOS:
(I did a similar dive for Android here: #2803)

Isolated Issue

I isolated one specific spot where I could reproduce an unexpected layout event.
After presenting the green modal, the first render shows the container view at correct height. But on ANY next re-render, the view jumps to the incorrect height. The minimal example here only updates the count inside Text, with state isolated inside its own component. No other components get re-rendered, or styles updated from JS.

I verified this by logging inside the render method of react-native-screens Screen.tsx, to make sure this component was not re-rendered from JS side. We see no renders, but we get the onLayout callback to show our new incorrect height. This repro exists on minimal-ios-repro branch here

Note: Views using React Native's Animated components (like Button or TouchableOpacity) caused immediate double-rendering on load, so we had to remove those in our repro otherwise the view would immediately jump to the incorrect height. Interesting behavior in itself, worth noting.

Image

So where is this new incorrect screen height coming from?

It's hard to know exactly. All we can see in the stack trace when the 'bad layout' happens is that a new Fabric render cycle gets scheduled. So it's safe to say that the invalid height already existed in the Fabric ShadowTree state when the next render cycle occurs to update the Text - so the ShadowTree is definitely out of sync with our view.

Potential updateBounds issue

When debugging this issue, I saw that updateBounds was getting called a lot when presenting a new screen (I count 9 calls when presenting green modal route), and the values bounce around a bit before settling. We also see our (future) incorrect height value getting set at some point earlier.

logs

Are all these updateBounds calls necessary?
As an experiment I commented out the call to updateBounds inside viewDidLayoutSubviews, and while the view height is rendered a bit too tall, it does resolve the issue. No more unexpected layouts! No more jumpiness! And I no longer saw any of the related layout issues on iOS. Strange!

Workaround

So a workaround I found is to wrap all modal views with a view that manually sets the height (which is known) and comment out updateBounds natively.
Jpoliachik/NewArchNavLayoutIssues#1
This seems to work for us, I'd be very curious to hear how it works for others.
It may have unwanted side effects, and is clearly a hacky fix, but I wanted to surface for others to try.

Root Issue

This still doesn't answer the root question of why this is causing issues - and why this might cause other issues throughout the app. A few examples out of the many outstanding issues:

We see in updateBounds updateState is called directly on the Shadow Node, then setNeedsLayout called on the parent controller directly afterwards.

**auto** newState = react::RNSScreenState{RCTSizeFromCGSize(**self**.bounds.size), RCTPointFromCGPoint(CGPointMake(0, headerHeight))};
_state->updateState(std::move(newState));
UINavigationController *navctr = _controller.navigationController;
[navctr.view setNeedsLayout];

Is this potentially problematic in New Architecture? I'm guessing this must be what causes the Shadow Tree state to go out of sync with native views - if this gets called in rapid succession, React must not handle all updates as expected.

  1. Is there a better way to updateBounds so we still adjust to the height of the view controller when needed, but we minimize our calls to update layout state so it only updates when necessary, and doesn't cause any layout loops or jumpiness?
  2. Is it okay to call updateState directly on a Shadow Node outside of a scheduled render cycle in Fabric at all? Or should layout updates be handled in JS so we can let React diff and apply view updates. This should be possible to do synchronously now in New Architecture Fabric renderer, so I'm wondering if that might be the safer option.

Thanks for reading, I apologize for the wall of text but I wanted to outline all the info I collected while debugging and potentially spark a larger discussion on how to best move forward with New Architecture overall.

Steps to reproduce

  1. Present a modal with full-height content
  2. Observe the height is incorrect, and changes often

See https://github.com/Jpoliachik/NewArchNavLayoutIssues

Snack or a link to a repository

https://github.com/Jpoliachik/NewArchNavLayoutIssues/tree/minimal-ios-repro

Screens version

4.9.0

React Native version

0.76.7

Platforms

iOS

JavaScript runtime

None

Workflow

None

Architecture

Fabric (New Architecture)

Build type

None

Device

None

Device model

No response

Acknowledgements

Yes

Metadata

Metadata

Assignees

No one assigned

    Labels

    platform:iosIssue related to iOS part of the libraryrepro-providedA reproduction with a snack or repo is provided

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions