fix(iOS): correct RTL ScrollView offset on recycle in Fabric#55804
fix(iOS): correct RTL ScrollView offset on recycle in Fabric#55804benslimanh wants to merge 1 commit intofacebook:mainfrom
Conversation
|
Hi @benslimanh! Thank you for your pull request and welcome to our community. Action RequiredIn 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. ProcessIn 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 If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
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? |
…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>
…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
Summary:
Fixes #55768
When a
ScrollViewis recycled in RTL mode, resetting_scrollView.zoomScale = 1.0insideprepareForRecyclecauses UIKit to mutate_containerView.frame, pushing the content off-screen.Because the old
_contentSizestill matches the newcontentSize,updateState:hits an early return optimization and fails to correct the mutated frame.This PR invalidates the cached
_contentSizeby setting it toCGSizeZeroimmediately after thezoomScalereset. 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:
ScrollViewin an RTL layout no longer causes the content to offset to the left of the window. The ScrollView remains correctly positioned.