Skip to content

fix(iOS): correct RTL ScrollView offset on recycle in Fabric#55804

Closed
benslimanh wants to merge 1 commit intofacebook:mainfrom
benslimanh:fix-rtl-scrollview-recycle
Closed

fix(iOS): correct RTL ScrollView offset on recycle in Fabric#55804
benslimanh wants to merge 1 commit intofacebook:mainfrom
benslimanh:fix-rtl-scrollview-recycle

Conversation

@benslimanh
Copy link
Copy Markdown
Contributor

@benslimanh benslimanh commented Feb 27, 2026

Summary:

Fixes #55768

When a ScrollView is recycled in RTL mode, resetting _scrollView.zoomScale = 1.0 inside prepareForRecycle causes UIKit to mutate _containerView.frame, pushing the content off-screen.
Because the old _contentSize still matches the new contentSize, updateState: hits an early return optimization and fails to correct the mutated frame.

This PR invalidates the cached _contentSize by setting it to CGSizeZero immediately after the zoomScale reset. This bypasses the early return and forces a correct recalculation of the frame.

Credit: Full credit to @kligarski for debugging and pinpointing the exact mechanism in the original issue.

Changelog:

[IOS] [FIXED] - Fixed RTL ScrollView offset bug upon recycling in Fabric

Test Plan:

  1. Ran the reproducer app provided in issue [iOS] recycled ScrollView is incorrectly offset to the left of the window when in RTL #55768.
  2. Verified that navigating (Push/Pop) multiple times with a ScrollView in an RTL layout no longer causes the content to offset to the left of the window. The ScrollView remains correctly positioned.

@meta-cla
Copy link
Copy Markdown

meta-cla bot commented Feb 27, 2026

Hi @benslimanh!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@meta-cla
Copy link
Copy Markdown

meta-cla bot commented Feb 27, 2026

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Feb 27, 2026
@facebook-github-bot facebook-github-bot added the Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. label Feb 27, 2026
@meta-codesync
Copy link
Copy Markdown

meta-codesync bot commented Feb 27, 2026

@javache has imported this pull request. If you are a Meta employee, you can view this in D94682107.

@meta-codesync meta-codesync bot closed this in 6b7ef31 Feb 27, 2026
@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label Feb 27, 2026
@meta-codesync
Copy link
Copy Markdown

meta-codesync bot commented Feb 27, 2026

@javache merged this pull request in 6b7ef31.

@react-native-bot
Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @benslimanh in 6b7ef31

When will my fix make it into a release? | How to file a pick request?

@benslimanh benslimanh deleted the fix-rtl-scrollview-recycle branch February 27, 2026 20:59
kligarski added a commit to software-mansion/react-native-screens that referenced this pull request Mar 4, 2026
…3613)

## Description

This PR adds proper RTL (Right-to-Left) layout support for native bottom
tabs on both iOS and Android in react-native-screens.

Previously, bottom tabs did not support the RTL direction, which caused
incorrect tab ordering and layout in RTL locales (e.g. Arabic, Hebrew).

With this change bottom tabs now support changing layout direction. This
brings native behavior in line with expected platform RTL handling and
React Native layout conventions.

### Details

@kligarski:

#### Android
On Android, the direction works out-of-the-box as it's propagated
through view hierarchy. We pass the value of the `direction` prop
directly to `TabsHost` view.

#### iOS
##### Badges
Setting `semanticContentAttribute` for `_controller.tabBar` and
`_controller.view` is not enough, e.g. badges visible through liquid
glass lens are still in LTR.


https://github.com/user-attachments/assets/0a96b18b-ddfe-4dd3-99c1-4a08a89ad86d

To handle this, we tried using the same approach as in native stack - we
set `UIView`'s `appearanceWhenContainedInInstancesOfClasses` of the tab
bar ([details how it's handled in the header are
here](https://github.com/software-mansion/react-native-screens/pull/2185/changes#diff-e5ef5b6e29f17bca80b51bc0c5faef1a44bac24e00952b30ac822520213dc6a5R504)).

However, this does not work for tab bar & sidebar on iPad starting from
iOS 18 as it is not a part of `controller.tabBar`. `_UITabContentView`
is mounted under `controller.view`. Using
`appearanceWhenContainedInInstancesOfClasses` for `_UITabContentView`
(which is already sketchy as this is an internal UIKit class) helps with
the order of items in the tab bar but the sidebar appears on the wrong
side of the screen.

That's why I decided to use modern way to handle direction via trait
overrides. For iOS prior to 17, you need to apply overrides on parent
view controller ([see
here](https://developer.apple.com/documentation/uikit/uiviewcontroller/setoverridetraitcollection(_:forchild:)?language=objc)).
This isn't the cleanest solution as the controller changes property of
the other controller which might not be a controller belonging to
`react-native-screens` but I think that this is the lesser evil. If this
turns out to be problematic, we can consider introducing some kind of
`ScreensRootView` that will ensure that there is a top controller from
`screens`.

For iOS 17+, we can use
[`traitOverrides`](https://developer.apple.com/documentation/uikit/uiviewcontroller/traitoverrides-8u19n?language=objc)
directly on the controller - this works with the top tab bar/sidebar on
iPadOS 18+.

##### ScrollView
On iOS, there is a bug with content of the ScrollView being moved off
screen after tab changes. I've reported the issue to `react-native`:
facebook/react-native#55768. The fix has been
merged (facebook/react-native#55804) and should
be available in next `react-native` release.


https://github.com/user-attachments/assets/b033d4c7-bfbe-415b-a02c-66fcfed6d0d6

##### Bottom Accessory
There seems to be a bug with bottom accessory in RTL when search role is
NOT used for one of the tabs (Apple Music and Apple Podcasts use search
role so the bug isn't visible).

| no search role (bug) | search role (no bug) |
| --- | --- |
| <video
src="https://github.com/user-attachments/assets/f5a154c0-4e52-40ca-8263-add687c23e65"
/> | <video
src="https://github.com/user-attachments/assets/cddcfab4-90b5-4387-8bea-e9c97c3b29e7"
/>

This bug is reproducible in bare UIKit app on iOS 26.2. 


https://github.com/user-attachments/assets/d0a36a07-f6a9-4bfd-9ac9-b5721a70e134

I've added this to our internal board
(https://github.com/software-mansion/react-native-screens-labs/issues/986)
and we'll check whether it has been fixed in iOS 26.3/26.4 beta.

##### Top tab bar badges

On iPadOS 18+, there seems to be a bug with the initial position of the
badges. They move to correct position after a tab change. I added a
ticket on our internal board to check whether this is a native bug:
https://github.com/software-mansion/react-native-screens-labs/issues/991.


https://github.com/user-attachments/assets/09c25789-b67c-40a1-a3e0-688043cbe17e

##### Native localization vs `react-native` on iOS

> [!IMPORTANT]
>
> When RTL is forced via `I18nManager.forceRTL(true)` but the language
of the native app isn't an RTL language, the views related to containers
such as the tab bar/sidebar by default will remain in LTR on iOS. This
is because we want to rely on native mechanism for layout direction
which is the trait system instead of `semanticContentAttribute` (used by
regular `react-native` views) which should only define whether the view
should be flipped in RTL & does not propagate down the hierarchy.
`forceRTL` does not change the trait therefore containers use layout
direction of the native app. In order to support `forceRTL`, you should
use `direction={I18nManager.isRTL ? 'rtl' : 'ltr'}` (see our example
implementation in `BottomTabsContainer.tsx`). This will override the
trait from the app with layout direction from `react-native` and
propagate it down the hierarchy.

## Changes

- add `direction` prop to `TabsHost` and implement it for both platforms
- add `Test3598.tsx`

### Before
(iOS only because Android works out of the box).
<img width="132" height="286" alt="Simulator Screenshot - iPhone 17 Pro
Max - 2026-02-03 at 18 39 51"
src="https://github.com/user-attachments/assets/19082dc4-49c7-433c-8656-37113be18d9a"
/>

### After 
#### Android
| Android Test3598 |
| --- |
| <video
src="https://github.com/user-attachments/assets/07fa151c-94b8-465e-892b-af1aa74deb83"
/> |

#### iOS
| iOS Test3598 | iOS TestBottomTabs | iOS Test3288 |
| --- | --- | --- |
| <video
src="https://github.com/user-attachments/assets/6ab88d38-df51-4869-9698-97d1b2ac2079"
/> | <video
src="https://github.com/user-attachments/assets/91d4dffc-c7a3-458c-a973-c61fefc6a2e7"
/> | <video
src="https://github.com/user-attachments/assets/dc775be4-bc07-4a82-8b42-93f54683ba84"
/> |

## Test plan
Use `Test3598`, `TestBottomTabs`, `Test3288` (iOS).

Tested on:
- Android (RTL system language enabled)
- iOS (RTL simulator + device)

## Checklist

- [x] Included code example that can be used to test this change.
- [x] For visual changes, included screenshots / GIFs / recordings
documenting the change.
- [ ] Ensured that CI passes

---------

Co-authored-by: Ahmed Awaad <ashahin@aljazirabank.com.sa>
Co-authored-by: Krzysztof Ligarski <krzysztof.ligarski@swmansion.com>
zoontek pushed a commit to zoontek/react-native that referenced this pull request Mar 9, 2026
…k#55804)

Summary:
Fixes facebook#55768

When a `ScrollView` is recycled in RTL mode, resetting `_scrollView.zoomScale = 1.0` inside `prepareForRecycle` causes UIKit to mutate `_containerView.frame`, pushing the content off-screen.
Because the old `_contentSize` still matches the new `contentSize`, `updateState:` hits an early return optimization and fails to correct the mutated frame.

This PR invalidates the cached `_contentSize` by setting it to `CGSizeZero` immediately after the `zoomScale` reset. This bypasses the early return and forces a correct recalculation of the frame.

*Credit: Full credit to kligarski for debugging and pinpointing the exact mechanism in the original issue.*

## Changelog:

[IOS] [FIXED] - Fixed RTL ScrollView offset bug upon recycling in Fabric

Pull Request resolved: facebook#55804

Test Plan:
1. Ran the reproducer app provided in issue facebook#55768.
2. Verified that navigating (Push/Pop) multiple times with a `ScrollView` in an RTL layout no longer causes the content to offset to the left of the window. The ScrollView remains correctly positioned.

Reviewed By: cipolleschi

Differential Revision: D94682107

Pulled By: javache

fbshipit-source-id: 3603824dcf37858f2924c9414a0ad4fcca2aea68
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[iOS] recycled ScrollView is incorrectly offset to the left of the window when in RTL

3 participants