Skip to content

Commit 492efd6

Browse files
authored
feat(iOS, Tabs, ScreenStack): orientation for Tabs and ScreenStack (#3014)
## Description Adds orientation management for `Tabs`. Closes software-mansion/react-native-screens-labs#97. Moved from software-mansion/react-native-screens-labs#262. Overriding `supportedInterfaceOrientaitons` for `TabBarController` and `TabsScreenViewController` is not enough. UIKit calls these methods only once and does not enforce orientation they return. UIKit asks root view controller more often - that's why we already swizzled `supportedInterfaceOrientations` to get access to return value from root `UIViewController` (from react-native) in `ScreenStack` implementation (useful article about [UIKit's behavior](https://gist.github.com/SergLam/802dd8a354ff9b925bb3ede8cae1f644)). I decided to use our own methods to handle orientation instead of overriding `supportedInterfaceOrientations` for tabs-related view controllers and use swizzled `supportedInterfaceOrientation` in root to return appropriate orientation mask to UIKit. In `ScreenStack`, `orientation: 'default'` is mapped to `UIInterfaceOrientationMaskAllButUpsideDown` but this is correct only for iPhones (e.g. iPads use `UIInterfaceOrientationMaskAll`). There is no other option that maps to `UIInterfaceOrientationMaskAllButUpsideDown`. In new implementation, I added options for all `UIInterfaceOrientationMask` enum values and changed `'default'` into `'inherit'` to use as default value. This value means that view controller does not have any preference for orientation. When root is asked for `supportedInterfaceOrientations`, it checks if their last child conforms to `RNSOrientationProviding`. If so, it asks the child view controller for orientation. These calls are propagated down the hierarchy with the last child having priority. If the last child returns `Inherit`, view controller above returns its preference. If root receives `Inherit`, it uses mask defined in `Info.plist` ([docs](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/application(_:supportedinterfaceorientationsfor:)?language=objc)), otherwise it maps received `RNSOrientation` into `UIInterfaceOrientationMask`. For `ScreenStack`, we use old implementation of handling orientation at the screen level (we map result from its `supportedInterfaceOrientations` to `RNSOrientation`). Old stack does not support new props, including `RNSOrientationInherit`. ### Example ``` UIViewController (React) | | last child view controller's orientation | or orientation from Info.plist | RNSTabBarController | | selected view controller's orientation | or RNSOrientationInherit | RNSTabsScreenViewController | | last child view controller's orientation | or orientation provided via prop (defaults to RNSOrientationInherit) | RNSNavigationController | | top view controller's orientation | or RNSOrientationInherit | RNSScreen | | RNSOrientation mapped from UIInterfaceOrientationMask | returned from supportedInterfaceOrientations (previous implementation) | ... ``` In the future, we need to implement orientation management for new `Stack` implementation as well as `SplitView`. This should be straightforward. `Stack` implementation will be a mix of old `ScreenStack` and new `Tabs` implementations. `SplitView` implementation should take into account orientation defined via prop at host controller. > [!NOTE] > Changing orientation manually (editing and saving file) via config in `TestBottomTabs.tsx` seems buggy but it is due to "Refreshing..." bar in debug mode. If you change the prop in JS (e.g. with `setInterval`), it works as expected. > | Manually | In JS | > | --- | --- | > | <video src="https://github.com/user-attachments/assets/eef43c27-e6d0-475f-860e-6854e9b42c4f" /> | <video src="https://github.com/user-attachments/assets/5356a1ee-bb90-4811-a427-12a3e94109e4" /> | ## Changes - add `RNSOrientationProviding` protocol - implement it for tabs and screenstack ## Test code and steps to reproduce Run example app and switch between tabs/screens in nested stack. ## Checklist - [x] Included code example that can be used to test this change - [ ] Ensured that CI passes
1 parent c42ed3e commit 492efd6

27 files changed

+454
-11
lines changed

android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,11 @@ class TabScreenViewManager :
187187
view.iconResourceName = value
188188
}
189189

190+
override fun setOrientation(
191+
view: TabScreen,
192+
value: String?,
193+
) = Unit
194+
190195
companion object {
191196
const val REACT_CLASS = "RNSBottomTabsScreen"
192197
}

android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerDelegate.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ public void setProperty(T view, String propName, @Nullable Object value) {
6363
case "title":
6464
mViewManager.setTitle(view, value == null ? null : (String) value);
6565
break;
66+
case "orientation":
67+
mViewManager.setOrientation(view, (String) value);
68+
break;
6669
case "iconResourceName":
6770
mViewManager.setIconResourceName(view, value == null ? null : (String) value);
6871
break;

android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerInterface.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public interface RNSBottomTabsScreenManagerInterface<T extends View> {
2828
void setTabBarItemIconColor(T view, @Nullable Integer value);
2929
void setTabBarItemBadgeBackgroundColor(T view, @Nullable Integer value);
3030
void setTitle(T view, @Nullable String value);
31+
void setOrientation(T view, @Nullable String value);
3132
void setIconResourceName(T view, @Nullable String value);
3233
void setTabBarItemBadgeTextColor(T view, @Nullable Integer value);
3334
void setIconType(T view, @Nullable String value);

apps/src/tests/TestBottomTabs/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ const TAB_CONFIGS: TabConfiguration[] = [
5555
tabBarItemIconColor: Colors.RedDark120,
5656
iconResourceName: 'sym_call_missed', // Android specific
5757
title: 'Tab2',
58+
orientation: 'landscape',
5859
},
5960
component: Tab2,
6061
},
@@ -72,6 +73,7 @@ const TAB_CONFIGS: TabConfiguration[] = [
7273
},
7374
iconResourceName: 'sym_action_email', // Android specific
7475
title: 'Tab3',
76+
orientation: 'portrait',
7577
},
7678
component: Tab3,
7779
},
@@ -87,6 +89,7 @@ const TAB_CONFIGS: TabConfiguration[] = [
8789
iconResourceName: 'sym_action_chat', // Android specific
8890
title: 'Tab4',
8991
badgeValue: '',
92+
orientation: 'portrait',
9093
},
9194
component: Tab4,
9295
},

apps/src/tests/TestBottomTabs/tabs/Tab4.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,14 @@ export function Tab4() {
113113
<Stack.Screen
114114
name="Screen1"
115115
component={Screen1}
116-
options={{ headerTransparent: true }}
116+
options={{ headerTransparent: true, orientation: 'landscape' }}
117117
/>
118118
<Stack.Screen
119119
name="Screen2"
120120
component={Screen2}
121121
options={{
122122
headerLargeTitle: true,
123+
orientation: 'default',
123124
}}
124125
/>
125126
<Stack.Screen

ios/RNSEnums.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,15 @@ typedef NS_ENUM(NSInteger, RNSTabBarMinimizeBehavior) {
140140
RNSTabBarMinimizeBehaviorOnScrollUp,
141141
};
142142
#endif
143+
144+
typedef NS_ENUM(NSInteger, RNSOrientation) {
145+
RNSOrientationInherit,
146+
RNSOrientationAll,
147+
RNSOrientationAllButUpsideDown,
148+
RNSOrientationPortrait,
149+
RNSOrientationPortraitUp,
150+
RNSOrientationPortraitDown,
151+
RNSOrientationLandscape,
152+
RNSOrientationLandscapeLeft,
153+
RNSOrientationLandscapeRight,
154+
};

ios/RNSOrientationProviding.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#import "RNSEnums.h"
2+
3+
NS_ASSUME_NONNULL_BEGIN
4+
5+
@protocol RNSOrientationProviding
6+
7+
- (RNSOrientation)evaluateOrientation;
8+
9+
@end
10+
11+
NS_ASSUME_NONNULL_END

ios/RNSScreen.h

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
#import "RNSScreenContentWrapper.h"
77
#import "RNSScrollViewBehaviorOverriding.h"
88

9+
#if !TARGET_OS_TV
10+
#import "RNSOrientationProviding.h"
11+
#endif // !TARGET_OS_TV
12+
913
#if RCT_NEW_ARCH_ENABLED
1014
#import <React/RCTViewComponentView.h>
1115
#else
@@ -33,8 +37,13 @@ namespace react = facebook::react;
3337

3438
@class RNSScreenView;
3539

36-
@interface RNSScreen : UIViewController <RNSViewControllerDelegate>
37-
40+
@interface RNSScreen : UIViewController <
41+
RNSViewControllerDelegate
42+
#if !TARGET_OS_TV
43+
,
44+
RNSOrientationProviding
45+
#endif // !TARGET_OS_TV
46+
>
3847
- (instancetype)initWithView:(UIView *)view;
3948
- (UIViewController *)findChildVCForConfigAndTrait:(RNSWindowTrait)trait includingModals:(BOOL)includingModals;
4049
- (BOOL)hasNestedStack;

ios/RNSScreen.mm

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
#import <React/RCTUIManager.h>
2929
#import <React/RCTUIManagerUtils.h>
3030

31+
#import "RNSConversions.h"
3132
#import "RNSScreenFooter.h"
3233
#import "RNSScreenStack.h"
3334
#import "RNSScreenStackHeaderConfig.h"
@@ -1979,6 +1980,26 @@ - (void)presentViewController:(UIViewController *)viewControllerToPresent
19791980
[super presentViewController:viewControllerToPresent animated:flag completion:completion];
19801981
}
19811982

1983+
#pragma mark - RNSOrientationProviding
1984+
1985+
#if !TARGET_OS_TV
1986+
1987+
- (RNSOrientation)evaluateOrientation
1988+
{
1989+
if ([self.childViewControllers.lastObject respondsToSelector:@selector(evaluateOrientation)]) {
1990+
id<RNSOrientationProviding> child = static_cast<id<RNSOrientationProviding>>(self.childViewControllers.lastObject);
1991+
RNSOrientation childOrientation = [child evaluateOrientation];
1992+
1993+
if (childOrientation != RNSOrientationInherit) {
1994+
return childOrientation;
1995+
}
1996+
}
1997+
1998+
return rnscreens::conversion::RNSOrientationFromUIInterfaceOrientationMask([self supportedInterfaceOrientations]);
1999+
}
2000+
2001+
#endif // !TARGET_OS_TV
2002+
19822003
#ifdef RCT_NEW_ARCH_ENABLED
19832004
#pragma mark - Fabric specific
19842005

ios/RNSScreenStack.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
#import "RNSBottomTabsSpecialEffectsSupporting.h"
99
#import "RNSScreenContainer.h"
1010

11+
#if !TARGET_OS_TV
12+
#import "RNSOrientationProviding.h"
13+
#endif // !TARGET_OS_TV
14+
1115
#ifdef RNS_GAMMA_ENABLED
1216
#import "RNSFrameCorrectionProvider.h"
1317
#endif // RNS_GAMMA_ENABLED
@@ -17,6 +21,10 @@ NS_ASSUME_NONNULL_BEGIN
1721
@interface RNSNavigationController : UINavigationController <
1822
RNSViewControllerDelegate,
1923
RNSBottomTabsSpecialEffectsSupporting
24+
#if !TARGET_OS_TV
25+
,
26+
RNSOrientationProviding
27+
#endif // !TARGET_OS_TV
2028
#ifdef RNS_GAMMA_ENABLED
2129
,
2230
RNSFrameCorrectionProvider

0 commit comments

Comments
 (0)