From 5b3052c07c33a74fee159048abbe763bf3550e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Du=C5=BCy?= <91994767+alduzy@users.noreply.github.com> Date: Thu, 27 Jun 2024 09:53:08 +0200 Subject: [PATCH] fix(iOS): back button does not respect I18nManager & improve RTL handling in header (#2185) ## Description This PR intents to fix back button direction in RTL mode forced by I18nManager for testing purposes on iOS. When the application's language is changed using the `CFBundleDevelopmentRegion` the system is well aware of that, passes the information to React Native and Yoga. The result is a fully LTR / RTL compliant application. However, when the `RTL` mode is set using the React Native's `I18nManager.forceRTL()` method (for testing purposes I believe) The change is applied to react views only and the system itself does not change it's direction. In such case the only way for the native elements, such as `navigationBar` to adapt to the direction set by `I18nManager` is by manually applying the `semanticContentAttribute` based on `direction` prop passed by `react-navigation`. > [!note] In case there is another native `navigationBar` element apart from the one coming from `react-native-screens`, this change may override it's direction. Tested successfully on iOS 17.5 and 16.4 Fixes #1884 . ## Changes - removed faulty workaround - set correct semanticContentAttribute for navigationBar and it's contents - refactored how the searchbar direction is set ## Screenshots / GIFs ### Before ![image](https://github.com/software-mansion/react-native-screens/assets/91994767/1c67071d-92e4-4966-9ad8-362c54c4435b) ### After ![image](https://github.com/software-mansion/react-native-screens/assets/91994767/5cf40cf1-b762-4720-af33-0f939adac7c5) ## Test code and steps to reproduce - play around with LTR and RTL mode in the example app ## Checklist - [ ] Included code example that can be used to test this change - [ ] Updated TS types - [ ] Updated documentation: - [ ] https://github.com/software-mansion/react-native-screens/blob/main/guides/GUIDE_FOR_LIBRARY_AUTHORS.md - [ ] https://github.com/software-mansion/react-native-screens/blob/main/native-stack/README.md - [ ] https://github.com/software-mansion/react-native-screens/blob/main/src/types.tsx - [ ] https://github.com/software-mansion/react-native-screens/blob/main/src/native-stack/types.tsx - [ ] Ensured that CI passes ## Short story on how RTL management works in RN apps on iOS by @kkafar > [!note] > Basically we have two RTL management systems: native one & ReactNative's I18nManager. > > Native one works most likely by applying semantic content attribute to all the views / classes and impacting layout constraints, thus whole native application is laid out properly. > We can impact RTL on natively managed views by: > * setting semantic content attributes for particular view > * [setting semantic content attributes for particular class through appearance proxy](https://developer.apple.com/documentation/uikit/uiappearance?language=objc) > * [setting semantic content attributes for particular class contained in instances of another class](https://developer.apple.com/documentation/uikit/uiappearance/1615006-appearancewhencontainedin?language=objc) > > Topic's to research: > * how parent's semantic content attribute (set directly on given view) does affect subviews > * exact semantics of using appearance proxy object, their priority and resolve order. > > > React Native has notion of internationalisation manager (I18n) which works as follows: > > * The RTL is allowed by default. > * If RTL is not forced from JS, and allowed, then system language writing direction is resolved based on system calls and such obtained interface direction is passed down to Yoga *impacting all views that are laid out by Yoga*, but not purely native ones such as our header. Thus leading to conformity between system options and RN app options. > * If RTL is forced from JS and allowed, them Yoga managed views are laid out in RTL mode, while system managed ones respect the system settings. > * ForceRTL / AllowRTL flags are stored in `NSUserDefaults` application persistent storage, thus living through application restarts. These flags are RN specific, and system has no knowledge of them. > > React Navigation takes the value from RN I18n system and passes it down to our screen / stack / header components. --------- Co-authored-by: Kacper Kafara Co-authored-by: Kacper Kafara --- ios/RNSScreenStackHeaderConfig.mm | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index a5a84a1f7..34a5c62c8 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -494,8 +494,17 @@ + (void)updateViewController:(UIViewController *)vc config.direction == UISemanticContentAttributeForceRightToLeft) && // iOS 12 cancels swipe gesture when direction is changed. See #1091 navctr.view.semanticContentAttribute != config.direction) { + // This is needed for swipe back gesture direction navctr.view.semanticContentAttribute = config.direction; + + // This is responsible for the direction of the navigationBar and its contents navctr.navigationBar.semanticContentAttribute = config.direction; + [[UIButton appearanceWhenContainedInInstancesOfClasses:@[ navctr.navigationBar.class ]] + setSemanticContentAttribute:config.direction]; + [[UIView appearanceWhenContainedInInstancesOfClasses:@[ navctr.navigationBar.class ]] + setSemanticContentAttribute:config.direction]; + [[UISearchBar appearanceWhenContainedInInstancesOfClasses:@[ navctr.navigationBar.class ]] + setSemanticContentAttribute:config.direction]; } if (shouldHide) { @@ -606,8 +615,6 @@ + (void)updateViewController:(UIViewController *)vc #endif } #if !TARGET_OS_TV - // Workaround for the wrong rotation of back button arrow in RTL mode. - navitem.hidesBackButton = true; navitem.hidesBackButton = config.hideBackButton; #endif navitem.leftBarButtonItem = nil; @@ -665,13 +672,6 @@ + (void)updateViewController:(UIViewController *)vc } } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0), dispatch_get_main_queue(), ^{ - // Position the contents in the navigation bar, regarding to the direction. - for (UIView *view in navctr.navigationBar.subviews) { - view.semanticContentAttribute = config.direction; - } - }); - // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) navitem.title = config.title;