Skip to content

Commit

Permalink
fix(iOS): back button does not respect I18nManager & improve RTL hand…
Browse files Browse the repository at this point in the history
…ling 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: <!-- For adding new props to native-stack
-->
- [ ]
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 <kacperkafara@gmail.com>
Co-authored-by: Kacper Kafara <kacper.kafara@swmansion.com>
  • Loading branch information
3 people committed Jun 28, 2024
1 parent ef3f863 commit 5b3052c
Showing 1 changed file with 9 additions and 9 deletions.
18 changes: 9 additions & 9 deletions ios/RNSScreenStackHeaderConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 5b3052c

Please sign in to comment.