diff --git a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt index 9d35d8d31a..19315c7d4c 100644 --- a/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt +++ b/android/src/main/java/com/swmansion/rnscreens/gamma/tabs/TabScreenViewManager.kt @@ -187,6 +187,11 @@ class TabScreenViewManager : view.iconResourceName = value } + override fun setOrientation( + view: TabScreen, + value: String?, + ) = Unit + companion object { const val REACT_CLASS = "RNSBottomTabsScreen" } diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerDelegate.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerDelegate.java index 35a2414ae7..72f7e4d9e6 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerDelegate.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerDelegate.java @@ -63,6 +63,9 @@ public void setProperty(T view, String propName, @Nullable Object value) { case "title": mViewManager.setTitle(view, value == null ? null : (String) value); break; + case "orientation": + mViewManager.setOrientation(view, (String) value); + break; case "iconResourceName": mViewManager.setIconResourceName(view, value == null ? null : (String) value); break; diff --git a/android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerInterface.java b/android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerInterface.java index 432261a5a8..09997f3023 100644 --- a/android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerInterface.java +++ b/android/src/paper/java/com/facebook/react/viewmanagers/RNSBottomTabsScreenManagerInterface.java @@ -28,6 +28,7 @@ public interface RNSBottomTabsScreenManagerInterface { void setTabBarItemIconColor(T view, @Nullable Integer value); void setTabBarItemBadgeBackgroundColor(T view, @Nullable Integer value); void setTitle(T view, @Nullable String value); + void setOrientation(T view, @Nullable String value); void setIconResourceName(T view, @Nullable String value); void setTabBarItemBadgeTextColor(T view, @Nullable Integer value); void setIconType(T view, @Nullable String value); diff --git a/apps/src/tests/TestBottomTabs/index.tsx b/apps/src/tests/TestBottomTabs/index.tsx index 1508684591..58f54766a3 100644 --- a/apps/src/tests/TestBottomTabs/index.tsx +++ b/apps/src/tests/TestBottomTabs/index.tsx @@ -55,6 +55,7 @@ const TAB_CONFIGS: TabConfiguration[] = [ tabBarItemIconColor: Colors.RedDark120, iconResourceName: 'sym_call_missed', // Android specific title: 'Tab2', + orientation: 'landscape', }, component: Tab2, }, @@ -72,6 +73,7 @@ const TAB_CONFIGS: TabConfiguration[] = [ }, iconResourceName: 'sym_action_email', // Android specific title: 'Tab3', + orientation: 'portrait', }, component: Tab3, }, @@ -87,6 +89,7 @@ const TAB_CONFIGS: TabConfiguration[] = [ iconResourceName: 'sym_action_chat', // Android specific title: 'Tab4', badgeValue: '', + orientation: 'portrait', }, component: Tab4, }, diff --git a/apps/src/tests/TestBottomTabs/tabs/Tab4.tsx b/apps/src/tests/TestBottomTabs/tabs/Tab4.tsx index acb3bed5ab..30681f73fa 100644 --- a/apps/src/tests/TestBottomTabs/tabs/Tab4.tsx +++ b/apps/src/tests/TestBottomTabs/tabs/Tab4.tsx @@ -113,13 +113,14 @@ export function Tab4() { #else @@ -33,8 +37,13 @@ namespace react = facebook::react; @class RNSScreenView; -@interface RNSScreen : UIViewController - +@interface RNSScreen : UIViewController < + RNSViewControllerDelegate +#if !TARGET_OS_TV + , + RNSOrientationProviding +#endif // !TARGET_OS_TV + > - (instancetype)initWithView:(UIView *)view; - (UIViewController *)findChildVCForConfigAndTrait:(RNSWindowTrait)trait includingModals:(BOOL)includingModals; - (BOOL)hasNestedStack; diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index 2aee8f91c1..1518827acb 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -28,6 +28,7 @@ #import #import +#import "RNSConversions.h" #import "RNSScreenFooter.h" #import "RNSScreenStack.h" #import "RNSScreenStackHeaderConfig.h" @@ -1979,6 +1980,26 @@ - (void)presentViewController:(UIViewController *)viewControllerToPresent [super presentViewController:viewControllerToPresent animated:flag completion:completion]; } +#pragma mark - RNSOrientationProviding + +#if !TARGET_OS_TV + +- (RNSOrientation)evaluateOrientation +{ + if ([self.childViewControllers.lastObject respondsToSelector:@selector(evaluateOrientation)]) { + id child = static_cast>(self.childViewControllers.lastObject); + RNSOrientation childOrientation = [child evaluateOrientation]; + + if (childOrientation != RNSOrientationInherit) { + return childOrientation; + } + } + + return rnscreens::conversion::RNSOrientationFromUIInterfaceOrientationMask([self supportedInterfaceOrientations]); +} + +#endif // !TARGET_OS_TV + #ifdef RCT_NEW_ARCH_ENABLED #pragma mark - Fabric specific diff --git a/ios/RNSScreenStack.h b/ios/RNSScreenStack.h index 4f6f2dae32..26407b9608 100644 --- a/ios/RNSScreenStack.h +++ b/ios/RNSScreenStack.h @@ -8,6 +8,10 @@ #import "RNSBottomTabsSpecialEffectsSupporting.h" #import "RNSScreenContainer.h" +#if !TARGET_OS_TV +#import "RNSOrientationProviding.h" +#endif // !TARGET_OS_TV + #ifdef RNS_GAMMA_ENABLED #import "RNSFrameCorrectionProvider.h" #endif // RNS_GAMMA_ENABLED @@ -17,6 +21,10 @@ NS_ASSUME_NONNULL_BEGIN @interface RNSNavigationController : UINavigationController < RNSViewControllerDelegate, RNSBottomTabsSpecialEffectsSupporting +#if !TARGET_OS_TV + , + RNSOrientationProviding +#endif // !TARGET_OS_TV #ifdef RNS_GAMMA_ENABLED , RNSFrameCorrectionProvider diff --git a/ios/RNSScreenStack.mm b/ios/RNSScreenStack.mm index 02c1c2f53b..18c65cc933 100644 --- a/ios/RNSScreenStack.mm +++ b/ios/RNSScreenStack.mm @@ -105,6 +105,20 @@ - (UIInterfaceOrientationMask)supportedInterfaceOrientations return [self topViewController].supportedInterfaceOrientations; } +#if !TARGET_OS_TV + +- (RNSOrientation)evaluateOrientation +{ + if ([self.topViewController respondsToSelector:@selector(evaluateOrientation)]) { + id top = static_cast>(self.topViewController); + return [top evaluateOrientation]; + } + + return RNSOrientationInherit; +} + +#endif // !TARGET_OS_TV + - (UIViewController *)childViewControllerForHomeIndicatorAutoHidden { return [self topViewController]; diff --git a/ios/UIViewController+RNScreens.mm b/ios/UIViewController+RNScreens.mm index 3c52f3aad3..52850a93fd 100644 --- a/ios/UIViewController+RNScreens.mm +++ b/ios/UIViewController+RNScreens.mm @@ -1,3 +1,6 @@ +#import "RNSConversions.h" +#import "RNSEnums.h" +#import "RNSOrientationProviding.h" #import "RNSScreenContainer.h" #import "UIViewController+RNScreens.h" @@ -27,8 +30,18 @@ - (UIStatusBarAnimation)reactNativeScreensPreferredStatusBarUpdateAnimation - (UIInterfaceOrientationMask)reactNativeScreensSupportedInterfaceOrientations { - UIViewController *childVC = [self findChildRNSScreensViewController]; - return childVC ? childVC.supportedInterfaceOrientations : [self reactNativeScreensSupportedInterfaceOrientations]; + id childOrientationProvidingVC = [self findChildRNSOrientationProvidingViewController]; + + if (childOrientationProvidingVC != nil) { + RNSOrientation orientation = [childOrientationProvidingVC evaluateOrientation]; + if (orientation == RNSOrientationInherit) { + return [[UIApplication sharedApplication] supportedInterfaceOrientationsForWindow:self.view.window]; + } + + return rnscreens::conversion::UIInterfaceOrientationMaskFromRNSOrientation(orientation); + } + + return [self reactNativeScreensSupportedInterfaceOrientations]; } - (UIViewController *)reactNativeScreensChildViewControllerForHomeIndicatorAutoHidden @@ -37,6 +50,15 @@ - (UIViewController *)reactNativeScreensChildViewControllerForHomeIndicatorAutoH return childVC ?: [self reactNativeScreensChildViewControllerForHomeIndicatorAutoHidden]; } +- (id)findChildRNSOrientationProvidingViewController +{ + UIViewController *lastViewController = [[self childViewControllers] lastObject]; + if ([lastViewController respondsToSelector:@selector(evaluateOrientation)]) { + return static_cast>(lastViewController); + } + return nil; +} + - (UIViewController *)findChildRNSScreensViewController { UIViewController *lastViewController = [[self childViewControllers] lastObject]; diff --git a/ios/bottom-tabs/RCTConvert+RNSBottomTabs.h b/ios/bottom-tabs/RCTConvert+RNSBottomTabs.h index 4cf64c7122..f0cf3aa9ae 100644 --- a/ios/bottom-tabs/RCTConvert+RNSBottomTabs.h +++ b/ios/bottom-tabs/RCTConvert+RNSBottomTabs.h @@ -11,6 +11,8 @@ NS_ASSUME_NONNULL_BEGIN + (RNSBottomTabsIconType)RNSBottomTabsIconType:(nonnull id)json; ++ (RNSOrientation)RNSOrientation:(nonnull id)json; + @end NS_ASSUME_NONNULL_END diff --git a/ios/bottom-tabs/RCTConvert+RNSBottomTabs.mm b/ios/bottom-tabs/RCTConvert+RNSBottomTabs.mm index 7a7e65b114..4e25f743d1 100644 --- a/ios/bottom-tabs/RCTConvert+RNSBottomTabs.mm +++ b/ios/bottom-tabs/RCTConvert+RNSBottomTabs.mm @@ -30,6 +30,22 @@ + (UIOffset)UIOffset:(id)json; RNSTabBarMinimizeBehaviorAutomatic, integerValue) +RCT_ENUM_CONVERTER( + RNSOrientation, + (@{ + @"inherit" : @(RNSOrientationInherit), + @"all" : @(RNSOrientationAll), + @"allButUpsideDown" : @(RNSOrientationAllButUpsideDown), + @"portrait" : @(RNSOrientationPortrait), + @"portraitUp" : @(RNSOrientationPortraitUp), + @"portraitDown" : @(RNSOrientationPortraitDown), + @"landscape" : @(RNSOrientationLandscape), + @"landscapeLeft" : @(RNSOrientationLandscapeLeft), + @"landscapeRight" : @(RNSOrientationLandscapeRight), + }), + RNSOrientationInherit, + integerValue) + @end #endif // !RCT_NEW_ARCH_ENABLED diff --git a/ios/bottom-tabs/RNSBottomTabsScreenComponentView.h b/ios/bottom-tabs/RNSBottomTabsScreenComponentView.h index fcf425953f..e0972a7c79 100644 --- a/ios/bottom-tabs/RNSBottomTabsScreenComponentView.h +++ b/ios/bottom-tabs/RNSBottomTabsScreenComponentView.h @@ -72,6 +72,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, nullable) UIColor *tabBarItemBadgeBackgroundColor; @property (nonatomic, nullable) NSString *title; +@property (nonatomic, readonly) RNSOrientation orientation; @property (nonatomic) BOOL shouldUseRepeatedTabSelectionPopToRootSpecialEffect; @property (nonatomic) BOOL shouldUseRepeatedTabSelectionScrollToTopSpecialEffect; diff --git a/ios/bottom-tabs/RNSBottomTabsScreenComponentView.mm b/ios/bottom-tabs/RNSBottomTabsScreenComponentView.mm index 9fd4afc3b7..af1d3e785a 100644 --- a/ios/bottom-tabs/RNSBottomTabsScreenComponentView.mm +++ b/ios/bottom-tabs/RNSBottomTabsScreenComponentView.mm @@ -30,6 +30,7 @@ @implementation RNSBottomTabsScreenComponentView { BOOL _isOverrideScrollViewContentInsetAdjustmentBehaviorSet; #if !RCT_NEW_ARCH_ENABLED BOOL _tabItemNeedsAppearanceUpdate; + BOOL _tabScreenOrientationNeedsUpdate; #endif // !RCT_NEW_ARCH_ENABLED } @@ -56,6 +57,7 @@ - (void)initState #if !RCT_NEW_ARCH_ENABLED _tabItemNeedsAppearanceUpdate = NO; + _tabScreenOrientationNeedsUpdate = NO; #endif // This is a temporary workaround to avoid UIScrollEdgeEffect glitch @@ -72,6 +74,7 @@ - (void)resetProps _badgeValue = nil; _title = nil; _tabBarBlurEffect = RNSBlurEffectStyleSystemDefault; + _orientation = RNSOrientationInherit; _tabBarBackgroundColor = nil; _tabBarItemTitleFontFamily = nil; @@ -152,12 +155,19 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props const auto &newComponentProps = *std::static_pointer_cast(props); bool tabItemNeedsAppearanceUpdate{false}; + bool tabScreenOrientationNeedsUpdate{false}; if (newComponentProps.title != oldComponentProps.title) { _title = RCTNSStringFromStringNilIfEmpty(newComponentProps.title); _controller.title = _title; } + if (newComponentProps.orientation != oldComponentProps.orientation) { + _orientation = + rnscreens::conversion::RNSOrientationFromRNSBottomTabsScreenOrientation(newComponentProps.orientation); + tabScreenOrientationNeedsUpdate = YES; + } + if (newComponentProps.tabKey != oldComponentProps.tabKey) { RCTAssert(!newComponentProps.tabKey.empty(), @"[RNScreens] tabKey must not be empty!"); _tabKey = RCTNSStringFromString(newComponentProps.tabKey); @@ -291,6 +301,10 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props [_controller tabItemAppearanceHasChanged]; } + if (tabScreenOrientationNeedsUpdate) { + [_controller tabScreenOrientationHasChanged]; + } + [super updateProps:props oldProps:oldProps]; } @@ -353,6 +367,11 @@ - (void)didSetProps:(NSArray *)changedProps [_controller tabItemAppearanceHasChanged]; _tabItemNeedsAppearanceUpdate = NO; } + + if (_tabScreenOrientationNeedsUpdate) { + [_controller tabScreenOrientationHasChanged]; + _tabScreenOrientationNeedsUpdate = NO; + } } #pragma mark - LEGACY prop setters @@ -480,6 +499,12 @@ - (void)setOverrideScrollViewContentInsetAdjustmentBehavior:(BOOL)overrideScroll // when the prop is undefined in JS and default value is used instead of calling this setter. } +- (void)setOrientation:(RNSOrientation)orientation +{ + _orientation = orientation; + _tabScreenOrientationNeedsUpdate = YES; +} + - (void)setOnWillAppear:(RCTDirectEventBlock)onWillAppear { [self.reactEventEmitter setOnWillAppear:onWillAppear]; diff --git a/ios/bottom-tabs/RNSBottomTabsScreenComponentViewManager.mm b/ios/bottom-tabs/RNSBottomTabsScreenComponentViewManager.mm index 56c6dca843..80215ee9b2 100644 --- a/ios/bottom-tabs/RNSBottomTabsScreenComponentViewManager.mm +++ b/ios/bottom-tabs/RNSBottomTabsScreenComponentViewManager.mm @@ -21,6 +21,7 @@ - (UIView *)view RCT_REMAP_VIEW_PROPERTY(isFocused, isSelectedScreen, BOOL); RCT_EXPORT_VIEW_PROPERTY(title, NSString); +RCT_EXPORT_VIEW_PROPERTY(orientation, RNSOrientation); RCT_EXPORT_VIEW_PROPERTY(badgeValue, NSString); RCT_EXPORT_VIEW_PROPERTY(tabBarBackgroundColor, UIColor); RCT_EXPORT_VIEW_PROPERTY(tabBarBlurEffect, RNSBlurEffectStyle); diff --git a/ios/bottom-tabs/RNSTabBarController.h b/ios/bottom-tabs/RNSTabBarController.h index 8c79a71de2..2db7e63803 100644 --- a/ios/bottom-tabs/RNSTabBarController.h +++ b/ios/bottom-tabs/RNSTabBarController.h @@ -2,6 +2,10 @@ #import "RNSTabBarAppearanceCoordinator.h" #import "RNSTabsScreenViewController.h" +#if !TARGET_OS_TV +#import "RNSOrientationProviding.h" +#endif // !TARGET_OS_TV + NS_ASSUME_NONNULL_BEGIN @protocol RNSReactTransactionObserving @@ -21,7 +25,13 @@ NS_ASSUME_NONNULL_BEGIN * i.e. if you made changes through one of signals method, unless you flush them immediately (not needed atm), they will * be executed only after react finishes the transaction (from within transaction execution block). */ -@interface RNSTabBarController : UITabBarController +@interface RNSTabBarController : UITabBarController < + RNSReactTransactionObserving +#if !TARGET_OS_TV + , + RNSOrientationProviding +#endif // !TARGET_OS_TV + > - (instancetype)initWithTabsHostComponentView:(nullable RNSBottomTabsHostComponentView *)tabsHostComponentView; @@ -58,7 +68,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Find out which tab bar controller is currently focused & select it. * - * This method does nothing if the update has not been previoulsy requested. + * This method does nothing if the update has not been previously requested. * If needed, the requested update is performed immediately. If you do not need this, consider just raising an * appropriate invalidation signal & let the controller decide when to flush the updates. */ @@ -75,7 +85,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Updates the tab bar appearance basing on configuration sources (host view, tab screens). * - * This method does nothing if the update has not been previoulsy requested. + * This method does nothing if the update has not been previously requested. * If needed, the requested update is performed immediately. If you do not need this, consider just raising an * appropriate invalidation signal & let the controller decide when to flush the updates. */ @@ -89,6 +99,23 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)updateTabBarAppearance; +/** + * Updates the interface orientation based on selected tab screen and its children. + * + * This method does nothing if the update has not been previously requested. + * If needed, the requested update is performed immediately. If you do not need this, consider just raising an + * appropriate invalidation signal & let the controller decide when to flush the updates. + */ +- (void)updateOrientationIfNeeded; + +/** + * Updates the interface orientation based on selected tab screen and its children. + * + * The requested update is performed immediately. If you do not need this, consider just raising an appropriate + * invalidation signal & let the controller decide when to flush the updates. + */ +- (void)updateOrientation; + @end #pragma mark - Signals @@ -101,7 +128,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be - * udpated. + * updated. * * This also automatically raises `needsReactChildrenUpdate` flag, no need to call it manually. */ @@ -109,7 +136,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be - * udpated. + * updated. * * Do not raise this signal only when focused state of the tab has changed - use `needsSelectedTabUpdate` instead. */ @@ -117,7 +144,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Tell the controller that react provided tabs have changed (count / instances) & the child view controllers need to be - * udpated. + * updated. */ @property (nonatomic, readwrite) bool needsUpdateOfSelectedTab; @@ -127,6 +154,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readwrite) bool needsUpdateOfTabBarAppearance; +/** + * Tell the controller that some configuration regarding interface orientation has changed & it requires update. + */ +@property (nonatomic, readwrite) bool needsOrientationUpdate; + @end NS_ASSUME_NONNULL_END diff --git a/ios/bottom-tabs/RNSTabBarController.mm b/ios/bottom-tabs/RNSTabBarController.mm index 8c2b0c785d..4e49a5cae6 100644 --- a/ios/bottom-tabs/RNSTabBarController.mm +++ b/ios/bottom-tabs/RNSTabBarController.mm @@ -1,6 +1,7 @@ #import "RNSTabBarController.h" #import #import +#import "RNSScreenWindowTraits.h" @implementation RNSTabBarController { NSArray *_Nullable _tabScreenControllers; @@ -57,6 +58,10 @@ - (void)setNeedsUpdateOfReactChildrenControllers:(bool)needsReactChildrenUpdate - (void)setNeedsUpdateOfSelectedTab:(bool)needsSelectedTabUpdate { _needsUpdateOfSelectedTab = needsSelectedTabUpdate; + + if (needsSelectedTabUpdate) { + _needsOrientationUpdate = true; + } #if !RCT_NEW_ARCH_ENABLED [self scheduleControllerUpdateIfNeeded]; #endif // !RCT_NEW_ARCH_ENABLED @@ -70,6 +75,14 @@ - (void)setNeedsUpdateOfTabBarAppearance:(bool)needsUpdateOfTabBarAppearance #endif // !RCT_NEW_ARCH_ENABLED } +- (void)setNeedsOrientationUpdate:(bool)needsOrientationUpdate +{ + _needsOrientationUpdate = needsOrientationUpdate; +#if !RCT_NEW_ARCH_ENABLED + [self scheduleControllerUpdateIfNeeded]; +#endif // !RCT_NEW_ARCH_ENABLED +} + #pragma mark-- RNSReactTransactionObserving - (void)reactMountingTransactionWillMount @@ -83,6 +96,7 @@ - (void)reactMountingTransactionDidMount [self updateReactChildrenControllersIfNeeded]; [self updateSelectedViewControllerIfNeeded]; [self updateTabBarAppearanceIfNeeded]; + [self updateOrientationIfNeeded]; } #pragma mark-- Signals related @@ -203,4 +217,33 @@ - (void)scheduleControllerUpdateIfNeeded #endif // !RCT_NEW_ARCH_ENABLED +- (void)updateOrientationIfNeeded +{ + if (_needsOrientationUpdate) { + [self updateOrientation]; + } +} + +- (void)updateOrientation +{ + _needsOrientationUpdate = false; + [RNSScreenWindowTraits enforceDesiredDeviceOrientation]; +} + +#pragma mark - RNSOrientationProviding + +#if !TARGET_OS_TV + +- (RNSOrientation)evaluateOrientation +{ + if ([self.selectedViewController respondsToSelector:@selector(evaluateOrientation)]) { + id selected = static_cast>(self.selectedViewController); + return [selected evaluateOrientation]; + } + + return RNSOrientationInherit; +} + +#endif // !TARGET_OS_TV + @end diff --git a/ios/bottom-tabs/RNSTabBarControllerDelegate.mm b/ios/bottom-tabs/RNSTabBarControllerDelegate.mm index d27f61612b..bf98c97176 100644 --- a/ios/bottom-tabs/RNSTabBarControllerDelegate.mm +++ b/ios/bottom-tabs/RNSTabBarControllerDelegate.mm @@ -48,6 +48,8 @@ - (BOOL)tabBarController:(UITabBarController *)tabBarController return ![self shouldPreventNativeTabChangeWithinTabBarController:tabBarCtrl]; } + // TODO: handle enforcing orientation with natively-driven tabs + // As we're selecting the same controller, returning both true and false works here. return true; } diff --git a/ios/bottom-tabs/RNSTabsScreenViewController.h b/ios/bottom-tabs/RNSTabsScreenViewController.h index 0b2438fc00..d1c31164fa 100644 --- a/ios/bottom-tabs/RNSTabsScreenViewController.h +++ b/ios/bottom-tabs/RNSTabsScreenViewController.h @@ -2,9 +2,16 @@ #import "RNSBottomTabsScreenComponentView.h" #import "RNSBottomTabsSpecialEffectsSupporting.h" +#if !TARGET_OS_TV +#import "RNSOrientationProviding.h" +#endif // !TARGET_OS_TV + NS_ASSUME_NONNULL_BEGIN @interface RNSTabsScreenViewController : UIViewController +#if !TARGET_OS_TV + +#endif // !TARGET_OS_TV @property (nonatomic, strong, readonly, nullable) RNSBottomTabsScreenComponentView *tabScreenComponentView; @property (nonatomic, weak, readonly, nullable) id tabsSpecialEffectsDelegate; @@ -19,6 +26,11 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)tabItemAppearanceHasChanged; +/** + * Tell the controller that the tab screen it owns has got its react-props-orientation changed. + */ +- (void)tabScreenOrientationHasChanged; + /** * Tell the controller that the tab item related to this controller has been selected again after being presented. * Returns boolean indicating whether the action has been handled. diff --git a/ios/bottom-tabs/RNSTabsScreenViewController.mm b/ios/bottom-tabs/RNSTabsScreenViewController.mm index 24cf45ee19..c55d93238d 100644 --- a/ios/bottom-tabs/RNSTabsScreenViewController.mm +++ b/ios/bottom-tabs/RNSTabsScreenViewController.mm @@ -32,6 +32,11 @@ - (void)tabItemAppearanceHasChanged [[self findTabBarController] setNeedsUpdateOfTabBarAppearance:true]; } +- (void)tabScreenOrientationHasChanged +{ + [[self findTabBarController] setNeedsOrientationUpdate:true]; +} + - (void)viewWillAppear:(BOOL)animated { [self.tabScreenComponentView.reactEventEmitter emitOnWillAppear]; @@ -108,4 +113,22 @@ - (bool)tabScreenSelectedRepeatedly return false; } +#if !TARGET_OS_TV + +- (RNSOrientation)evaluateOrientation +{ + if ([self.childViewControllers.lastObject respondsToSelector:@selector(evaluateOrientation)]) { + id child = static_cast>(self.childViewControllers.lastObject); + RNSOrientation childOrientation = [child evaluateOrientation]; + + if (childOrientation != RNSOrientationInherit) { + return childOrientation; + } + } + + return self.tabScreenComponentView.orientation; +} + +#endif // !TARGET_OS_TV + @end diff --git a/ios/conversion/RNSConversions-BottomTabs.mm b/ios/conversion/RNSConversions-BottomTabs.mm index de6e616e80..73ed4f642c 100644 --- a/ios/conversion/RNSConversions-BottomTabs.mm +++ b/ios/conversion/RNSConversions-BottomTabs.mm @@ -287,4 +287,33 @@ RNSBottomTabsIconType RNSBottomTabsIconTypeFromIcon(react::RNSBottomTabsScreenIc return iconImageSource; } +RNSOrientation RNSOrientationFromRNSBottomTabsScreenOrientation(react::RNSBottomTabsScreenOrientation orientation) +{ + using enum facebook::react::RNSBottomTabsScreenOrientation; + + switch (orientation) { + case Inherit: + return RNSOrientationInherit; + case All: + return RNSOrientationAll; + case AllButUpsideDown: + return RNSOrientationAllButUpsideDown; + case Portrait: + return RNSOrientationPortrait; + case PortraitUp: + return RNSOrientationPortraitUp; + case PortraitDown: + return RNSOrientationPortraitDown; + case Landscape: + return RNSOrientationLandscape; + case LandscapeLeft: + return RNSOrientationLandscapeLeft; + case LandscapeRight: + return RNSOrientationLandscapeRight; + default: + RCTLogError(@"[RNScreens] unsupported orientation"); + return RNSOrientationInherit; + } +} + }; // namespace rnscreens::conversion diff --git a/ios/conversion/RNSConversions.h b/ios/conversion/RNSConversions.h index 1e03834733..4030743133 100644 --- a/ios/conversion/RNSConversions.h +++ b/ios/conversion/RNSConversions.h @@ -55,6 +55,17 @@ RCTImageSource *RCTImageSourceFromImageSourceAndIconType( const facebook::react::ImageSource *imageSource, RNSBottomTabsIconType iconType); +RNSOrientation RNSOrientationFromRNSBottomTabsScreenOrientation( + react::RNSBottomTabsScreenOrientation orientation); + +#if !TARGET_OS_TV +UIInterfaceOrientationMask UIInterfaceOrientationMaskFromRNSOrientation( + RNSOrientation orientation); + +RNSOrientation RNSOrientationFromUIInterfaceOrientationMask( + UIInterfaceOrientationMask orientationMask); +#endif // !TARGET_OS_TV + #pragma mark SplitViewHost props UISplitViewControllerSplitBehavior SplitViewPreferredSplitBehaviorFromHostProp( diff --git a/ios/conversion/RNSConversions.mm b/ios/conversion/RNSConversions.mm new file mode 100644 index 0000000000..da07ab88df --- /dev/null +++ b/ios/conversion/RNSConversions.mm @@ -0,0 +1,61 @@ +#import "RNSConversions.h" +#import + +namespace rnscreens::conversion { + +#if !TARGET_OS_TV +UIInterfaceOrientationMask UIInterfaceOrientationMaskFromRNSOrientation(RNSOrientation orientation) +{ + switch (orientation) { + case RNSOrientationAll: + return UIInterfaceOrientationMaskAll; + case RNSOrientationAllButUpsideDown: + return UIInterfaceOrientationMaskAllButUpsideDown; + case RNSOrientationPortrait: + return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown; + case RNSOrientationPortraitUp: + return UIInterfaceOrientationMaskPortrait; + case RNSOrientationPortraitDown: + return UIInterfaceOrientationMaskPortraitUpsideDown; + case RNSOrientationLandscape: + return UIInterfaceOrientationMaskLandscape; + case RNSOrientationLandscapeLeft: + return UIInterfaceOrientationMaskLandscapeLeft; + case RNSOrientationLandscapeRight: + return UIInterfaceOrientationMaskLandscapeRight; + case RNSOrientationInherit: + RCTLogError(@"[RNScreens] RNSOrientationInherit does not map directly to any UIInterfaceOrientationMask"); + return 0; + default: + RCTLogError(@"[RNScreens] Unsupported orientation"); + return 0; + } +} + +RNSOrientation RNSOrientationFromUIInterfaceOrientationMask(UIInterfaceOrientationMask orientationMask) +{ + switch (orientationMask) { + case UIInterfaceOrientationMaskAll: + return RNSOrientationAll; + case UIInterfaceOrientationMaskAllButUpsideDown: + return RNSOrientationAllButUpsideDown; + case UIInterfaceOrientationMaskLandscape: + return RNSOrientationLandscape; + case UIInterfaceOrientationMaskLandscapeLeft: + return RNSOrientationLandscapeLeft; + case UIInterfaceOrientationLandscapeRight: + return RNSOrientationLandscapeRight; + case UIInterfaceOrientationMaskPortraitUpsideDown: + return RNSOrientationPortraitDown; + case UIInterfaceOrientationMaskPortrait: + return RNSOrientationPortraitUp; + case UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskPortraitUpsideDown: + return RNSOrientationPortrait; + default: + RCTLogError(@"[RNScreens] Unsupported orientation mask"); + return RNSOrientationInherit; + } +} +#endif // !TARGET_OS_TV + +}; // namespace rnscreens::conversion diff --git a/src/components/bottom-tabs/BottomTabsScreen.types.ts b/src/components/bottom-tabs/BottomTabsScreen.types.ts index 574aa7ca1f..2211426fbf 100644 --- a/src/components/bottom-tabs/BottomTabsScreen.types.ts +++ b/src/components/bottom-tabs/BottomTabsScreen.types.ts @@ -60,6 +60,18 @@ export type BottomTabsScreenBlurEffect = | 'systemThickMaterialDark' | 'systemChromeMaterialDark'; +// Currently iOS-only +export type BottomTabsScreenOrientation = + | 'inherit' + | 'all' + | 'allButUpsideDown' + | 'portrait' + | 'portraitUp' + | 'portraitDown' + | 'landscape' + | 'landscapeLeft' + | 'landscapeRight'; + export interface BottomTabsScreenProps { children?: ViewProps['children']; /** @@ -113,6 +125,55 @@ export interface BottomTabsScreenProps { * @platform android, ios */ badgeValue?: string; + /** + * @summary Specifies supported orientations for the tab screen. + * + * Procedure for determining supported orientations: + * 1. Traversal initiates from the root component and moves to the + * deepest child possible. + * 2. Components are queried for their supported orientations: + * - if `orientation` is explicitly set (e.g., `portrait`, + * `landscape`), it is immediately used, + * - if `orientation` is set to `inherit`, the parent component + * is queried. + * + * Note that: + * - some components (like `SplitViewHost`) may choose not to query + * its child components, + * - Stack v4 implementation **ALWAYS** returns some supported + * orientations (`allButUpsideDown` by default), overriding + * orientation from tab screen. + * + * The following values are currently supported: + * + * - `inherit` - tab screen supports the same orientations as parent + * component, + * - `all` - tab screen supports all orientations, + * - `allButUpsideDown` - tab screen supports all but the upside-down + * portrait interface orientation, + * - `portrait` - tab screen supports both portrait-up and portrait-down + * interface orientations, + * - 'portraitUp' - tab screen supports a portrait-up interface + * orientation, + * - `portraitDown` - tab screen supports a portrait-down interface + * orientation, + * - `landscape` - tab screen supports both landscape-left and + * landscape-right interface orientations, + * - `landscapeLeft` - tab screen supports landscape-left interface + * orientaion, + * - `landscapeRight` - tab screen supports landscape-right interface + * orientaion. + * + * The supported values (apart from `inherit`, `portrait`, `portraitUp`, + * `portraitDown`) correspond to the official UIKit documentation: + * + * @see {@link https://developer.apple.com/documentation/uikit/uiinterfaceorientationmask|UIInterfaceOrientationMask} + * + * @default inherit + * + * @platform ios + */ + orientation?: BottomTabsScreenOrientation; // #endregion General // #region Common appearance diff --git a/src/fabric/bottom-tabs/BottomTabsScreenNativeComponent.ts b/src/fabric/bottom-tabs/BottomTabsScreenNativeComponent.ts index 144a381c7e..5587cca7a8 100644 --- a/src/fabric/bottom-tabs/BottomTabsScreenNativeComponent.ts +++ b/src/fabric/bottom-tabs/BottomTabsScreenNativeComponent.ts @@ -47,6 +47,17 @@ type BlurEffect = | 'systemThickMaterialDark' | 'systemChromeMaterialDark'; +type Orientation = + | 'inherit' + | 'all' + | 'allButUpsideDown' + | 'portrait' + | 'portraitUp' + | 'portraitDown' + | 'landscape' + | 'landscapeLeft' + | 'landscapeRight'; + export interface NativeProps extends ViewProps { // Events onLifecycleStateChange?: DirectEventHandler; @@ -81,6 +92,9 @@ export interface NativeProps extends ViewProps { // General title?: string | undefined | null; + // Currently iOS-only + orientation?: WithDefault; + // Android-specific image handling iconResourceName?: string; tabBarItemBadgeTextColor?: ColorValue;