#ifdef RN_FABRIC_ENABLED #import #import #import #import #import #import #import #else #import #import #import #import #import #import #import #endif #import #import "RNSScreen.h" #import "RNSScreenStackHeaderConfig.h" #import "RNSSearchBar.h" #import "RNSUIBarButtonItem.h" #ifdef RN_FABRIC_ENABLED #else // Some RN private method hacking below. Couldn't figure out better way to access image data // of a given RCTImageView. See more comments in the code section processing SubviewTypeBackButton @interface RCTImageView (Private) - (UIImage *)image; @end @interface RCTImageLoader (Private) - (id)imageCache; @end #endif @implementation RNSScreenStackHeaderConfig { NSMutableArray *_reactSubviews; #ifdef RN_FABRIC_ENABLED BOOL _initialPropsSet; #else #endif } #ifdef RN_FABRIC_ENABLED - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); _props = defaultProps; self.hidden = YES; _show = YES; _translucent = NO; _reactSubviews = [NSMutableArray new]; } return self; } #else - (instancetype)init { if (self = [super init]) { self.hidden = YES; _translucent = YES; _reactSubviews = [NSMutableArray new]; } return self; } #endif - (UIView *)reactSuperview { return _screenView; } - (NSArray *)reactSubviews { return _reactSubviews; } - (void)removeFromSuperview { [super removeFromSuperview]; _screenView = nil; } // this method is never invoked by the system since this view // is not added to native view hierarchy so we can apply our logic - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { for (RNSScreenStackHeaderSubview *subview in _reactSubviews) { if (subview.type == RNSScreenStackHeaderSubviewTypeLeft || subview.type == RNSScreenStackHeaderSubviewTypeRight) { // we wrap the headerLeft/Right component in a UIBarButtonItem // so we need to use the only subview of it to retrieve the correct view UIView *headerComponent = subview.subviews.firstObject; // we convert the point to RNSScreenStackView since it always contains the header inside it CGPoint convertedPoint = [_screenView.reactSuperview convertPoint:point toView:headerComponent]; UIView *hitTestResult = [headerComponent hitTest:convertedPoint withEvent:event]; if (hitTestResult != nil) { return hitTestResult; } } } return nil; } - (void)updateViewControllerIfNeeded { UIViewController *vc = _screenView.controller; UINavigationController *nav = (UINavigationController *)vc.parentViewController; UIViewController *nextVC = nav.visibleViewController; if (nav.transitionCoordinator != nil) { // if navigator is performing transition instead of allowing to update of `visibleConttroller` // we look at `topController`. This is because during transitiong the `visibleController` won't // point to the controller that is going to be revealed after transition. This check fixes the // problem when config gets updated while the transition is ongoing. nextVC = nav.topViewController; } // we want updates sent to the VC below modal too since it is also visible BOOL isPresentingVC = nextVC != nil && vc.presentedViewController == nextVC; BOOL isInFullScreenModal = nav == nil && _screenView.stackPresentation == RNSScreenStackPresentationFullScreenModal; // if nav is nil, it means we can be in a fullScreen modal, so there is no nextVC, but we still want to update if (vc != nil && (nextVC == vc || isInFullScreenModal || isPresentingVC)) { [RNSScreenStackHeaderConfig updateViewController:self.screenView.controller withConfig:self animated:YES]; } } - (void)layoutNavigationControllerView { // We need to layout navigation controller view after translucent prop changes, because otherwise // frame of RNSScreen will not be changed and screen content will remain the same size. // For more details look at https://github.com/software-mansion/react-native-screens/issues/1158 UIViewController *vc = _screenView.controller; UINavigationController *navctr = vc.navigationController; [navctr.view setNeedsLayout]; } + (void)setAnimatedConfig:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config { UINavigationBar *navbar = ((UINavigationController *)vc.parentViewController).navigationBar; // It is workaround for loading custom back icon when transitioning from a screen without header to the screen which // has one. This action fails when navigating to the screen with header for the second time and loads default back // button. It looks like changing the tint color of navbar triggers an update of the items belonging to it and it // seems to load the custom back image so we change the tint color's alpha by a very small amount and then set it to // the one it should have. #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_14_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 // it brakes the behavior of `headerRight` in iOS 14, where the bug desribed above seems to be fixed, so we do nothing // in iOS 14 if (@available(iOS 14.0, *)) { } else #endif { [navbar setTintColor:[config.color colorWithAlphaComponent:CGColorGetAlpha(config.color.CGColor) - 0.01]]; } [navbar setTintColor:config.color]; #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 if (@available(iOS 13.0, *)) { // font customized on the navigation item level, so nothing to do here } else #endif { BOOL hideShadow = config.hideShadow; if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) { [navbar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; [navbar setBarTintColor:[UIColor clearColor]]; hideShadow = YES; } else { [navbar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; [navbar setBarTintColor:config.backgroundColor]; } [navbar setTranslucent:config.translucent]; [navbar setValue:@(hideShadow ? YES : NO) forKey:@"hidesShadow"]; if (config.titleFontFamily || config.titleFontSize || config.titleFontWeight || config.titleColor) { NSMutableDictionary *attrs = [NSMutableDictionary new]; if (config.titleColor) { attrs[NSForegroundColorAttributeName] = config.titleColor; } NSString *family = config.titleFontFamily ?: nil; NSNumber *size = config.titleFontSize ?: @17; NSString *weight = config.titleFontWeight ?: nil; if (family || weight) { attrs[NSFontAttributeName] = [RCTFont updateFont:nil withFamily:family size:size weight:weight style:nil variant:nil scaleMultiplier:1.0]; } else { attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:[size floatValue]]; } [navbar setTitleTextAttributes:attrs]; } #if !TARGET_OS_TV if (@available(iOS 11.0, *)) { if (config.largeTitle && (config.largeTitleFontFamily || config.largeTitleFontSize || config.largeTitleFontWeight || config.largeTitleColor || config.titleColor)) { NSMutableDictionary *largeAttrs = [NSMutableDictionary new]; if (config.largeTitleColor || config.titleColor) { largeAttrs[NSForegroundColorAttributeName] = config.largeTitleColor ? config.largeTitleColor : config.titleColor; } NSString *largeFamily = config.largeTitleFontFamily ?: nil; NSNumber *largeSize = config.largeTitleFontSize ?: @34; NSString *largeWeight = config.largeTitleFontWeight ?: nil; if (largeFamily || largeWeight) { largeAttrs[NSFontAttributeName] = [RCTFont updateFont:nil withFamily:largeFamily size:largeSize weight:largeWeight style:nil variant:nil scaleMultiplier:1.0]; } else { largeAttrs[NSFontAttributeName] = [UIFont systemFontOfSize:[largeSize floatValue] weight:UIFontWeightBold]; } [navbar setLargeTitleTextAttributes:largeAttrs]; } } #endif } } + (void)setTitleAttibutes:(NSDictionary *)attrs forButton:(UIBarButtonItem *)button { [button setTitleTextAttributes:attrs forState:UIControlStateNormal]; [button setTitleTextAttributes:attrs forState:UIControlStateHighlighted]; [button setTitleTextAttributes:attrs forState:UIControlStateDisabled]; [button setTitleTextAttributes:attrs forState:UIControlStateSelected]; [button setTitleTextAttributes:attrs forState:UIControlStateFocused]; } + (UIImage *)loadBackButtonImageInViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config { #ifdef RN_FABRIC_ENABLED @throw([NSException exceptionWithName:@"UNIMPLEMENTED" reason:@"Implement" userInfo:nil]); #else BOOL hasBackButtonImage = NO; for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) { if (subview.type == RNSScreenStackHeaderSubviewTypeBackButton && subview.subviews.count > 0) { hasBackButtonImage = YES; RCTImageView *imageView = subview.subviews[0]; if (imageView.image == nil) { // This is yet another workaround for loading custom back icon. It turns out that under // certain circumstances image attribute can be null despite the app running in production // mode (when images are loaded from the filesystem). This can happen because image attribute // is reset when image view is detached from window, and also in some cases initialization // does not populate the frame of the image view before the loading start. The latter result // in the image attribute not being updated. We manually set frame to the size of an image // in order to trigger proper reload that'd update the image attribute. RCTImageSource *source = imageView.imageSources[0]; [imageView reactSetFrame:CGRectMake( imageView.frame.origin.x, imageView.frame.origin.y, source.size.width, source.size.height)]; } UIImage *image = imageView.image; // IMPORTANT!!! // image can be nil in DEV MODE ONLY // // It is so, because in dev mode images are loaded over HTTP from the packager. In that case // we first check if image is already loaded in cache and if it is, we take it from cache and // display immediately. Otherwise we wait for the transition to finish and retry updating // header config. // Unfortunately due to some problems in UIKit we cannot update the image while the screen // transition is ongoing. This results in the settings being reset after the transition is done // to the state from before the transition. if (image == nil) { // in DEV MODE we try to load from cache (we use private API for that as it is not exposed // publically in headers). RCTImageSource *source = imageView.imageSources[0]; RCTImageLoader *imageloader = [subview.bridge moduleForClass:[RCTImageLoader class]]; image = [imageloader.imageCache imageForUrl:source.request.URL.absoluteString size:source.size scale:source.scale resizeMode:imageView.resizeMode]; } if (image == nil) { // This will be triggered if the image is not in the cache yet. What we do is we wait until // the end of transition and run header config updates again. We could potentially wait for // image on load to trigger, but that would require even more private method hacking. if (vc.transitionCoordinator) { [vc.transitionCoordinator animateAlongsideTransition:^(id _Nonnull context) { // nothing, we just want completion } completion:^(id _Nonnull context) { // in order for new back button image to be loaded we need to trigger another change // in back button props that'd make UIKit redraw the button. Otherwise the changes are // not reflected. Here we change back button visibility which is then immediately restored #if !TARGET_OS_TV vc.navigationItem.hidesBackButton = YES; #endif [config updateViewControllerIfNeeded]; }]; } return [UIImage new]; } else { return image; } } } #endif // RN_FABRIC_ENABLED return nil; } + (void)willShowViewController:(UIViewController *)vc animated:(BOOL)animated withConfig:(RNSScreenStackHeaderConfig *)config { [self updateViewController:vc withConfig:config animated:animated]; } #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 + (UINavigationBarAppearance *)buildAppearance:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config API_AVAILABLE(ios(13.0)) { UINavigationBarAppearance *appearance = [UINavigationBarAppearance new]; if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) { // transparent background color [appearance configureWithTransparentBackground]; } else { [appearance configureWithOpaqueBackground]; } // set background color if specified if (config.backgroundColor) { appearance.backgroundColor = config.backgroundColor; } // TODO: implement blurEffect on Fabric #ifdef RN_FABRIC_ENABLED #else if (config.blurEffect) { appearance.backgroundEffect = [UIBlurEffect effectWithStyle:config.blurEffect]; } #endif if (config.hideShadow) { appearance.shadowColor = nil; } if (config.titleFontFamily || config.titleFontSize || config.titleFontWeight || config.titleColor) { NSMutableDictionary *attrs = [NSMutableDictionary new]; if (config.titleColor) { attrs[NSForegroundColorAttributeName] = config.titleColor; } NSString *family = config.titleFontFamily ?: nil; NSNumber *size = config.titleFontSize ?: @17; NSString *weight = config.titleFontWeight ?: nil; if (family || weight) { attrs[NSFontAttributeName] = [RCTFont updateFont:nil withFamily:config.titleFontFamily size:size weight:weight style:nil variant:nil scaleMultiplier:1.0]; } else { attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:[size floatValue]]; } appearance.titleTextAttributes = attrs; } if (config.largeTitleFontFamily || config.largeTitleFontSize || config.largeTitleFontWeight || config.largeTitleColor || config.titleColor) { NSMutableDictionary *largeAttrs = [NSMutableDictionary new]; if (config.largeTitleColor || config.titleColor) { largeAttrs[NSForegroundColorAttributeName] = config.largeTitleColor ? config.largeTitleColor : config.titleColor; } NSString *largeFamily = config.largeTitleFontFamily ?: nil; NSNumber *largeSize = config.largeTitleFontSize ?: @34; NSString *largeWeight = config.largeTitleFontWeight ?: nil; if (largeFamily || largeWeight) { largeAttrs[NSFontAttributeName] = [RCTFont updateFont:nil withFamily:largeFamily size:largeSize weight:largeWeight style:nil variant:nil scaleMultiplier:1.0]; } else { largeAttrs[NSFontAttributeName] = [UIFont systemFontOfSize:[largeSize floatValue] weight:UIFontWeightBold]; } appearance.largeTitleTextAttributes = largeAttrs; } #ifdef RN_FABRIC_ENABLED [appearance setBackIndicatorImage:nil transitionMaskImage:nil]; #else UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config]; if (backButtonImage) { [appearance setBackIndicatorImage:backButtonImage transitionMaskImage:backButtonImage]; } else if (appearance.backIndicatorImage) { [appearance setBackIndicatorImage:nil transitionMaskImage:nil]; } #endif // RN_FABRIC_ENABLED return appearance; } #endif + (void)updateViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config animated:(BOOL)animated { UINavigationItem *navitem = vc.navigationItem; UINavigationController *navctr = (UINavigationController *)vc.parentViewController; NSUInteger currentIndex = [navctr.viewControllers indexOfObject:vc]; UINavigationItem *prevItem = currentIndex > 0 ? [navctr.viewControllers objectAtIndex:currentIndex - 1].navigationItem : nil; BOOL wasHidden = navctr.navigationBarHidden; #ifdef RN_FABRIC_ENABLED BOOL shouldHide = config == nil || !config.show; #else BOOL shouldHide = config == nil || config.hide; #endif if (!shouldHide && !config.translucent) { // when nav bar is not translucent we chage edgesForExtendedLayout to avoid system laying out // the screen underneath navigation controllers vc.edgesForExtendedLayout = UIRectEdgeNone; } else { // system default is UIRectEdgeAll vc.edgesForExtendedLayout = UIRectEdgeAll; } [navctr setNavigationBarHidden:shouldHide animated:animated]; if ((config.direction == UISemanticContentAttributeForceLeftToRight || config.direction == UISemanticContentAttributeForceRightToLeft) && // iOS 12 cancels swipe gesture when direction is changed. See #1091 navctr.view.semanticContentAttribute != config.direction) { navctr.view.semanticContentAttribute = config.direction; navctr.navigationBar.semanticContentAttribute = config.direction; } if (shouldHide) { return; } #if !TARGET_OS_TV // Fix for github.com/react-navigation/react-navigation/issues/11015 // It allows to hide back button title and use back button menu as normal. // Back button display mode and back button menu are available since iOS 14. if (@available(iOS 14.0, *)) { // Make sure to set display mode to default. // This line resets back button display mode - especially needed on the Fabric architecture. navitem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeDefault; NSString *trimmedBackTitle = [config.backTitle stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; // When an whitespace only back title is passed set back button mode to minimal. if (config.backTitle != nil && [trimmedBackTitle length] == 0) { navitem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeMinimal; } } else if ( config.backTitle != nil || config.backTitleFontFamily || config.backTitleFontSize || config.disableBackButtonMenu) { RNSUIBarButtonItem *backBarButtonItem = [[RNSUIBarButtonItem alloc] initWithTitle:config.backTitle ?: prevItem.title style:UIBarButtonItemStylePlain target:nil action:nil]; [backBarButtonItem setMenuHidden:config.disableBackButtonMenu]; prevItem.backBarButtonItem = backBarButtonItem; if (config.backTitleFontFamily || config.backTitleFontSize) { NSMutableDictionary *attrs = [NSMutableDictionary new]; NSNumber *size = config.backTitleFontSize ?: @17; if (config.backTitleFontFamily) { attrs[NSFontAttributeName] = [RCTFont updateFont:nil withFamily:config.backTitleFontFamily size:size weight:nil style:nil variant:nil scaleMultiplier:1.0]; } else { attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:[size floatValue]]; } [self setTitleAttibutes:attrs forButton:prevItem.backBarButtonItem]; } } else { prevItem.backBarButtonItem = nil; } if (@available(iOS 11.0, *)) { if (config.largeTitle) { navctr.navigationBar.prefersLargeTitles = YES; } navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever; } #endif #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 if (@available(iOS 13.0, tvOS 13.0, *)) { UINavigationBarAppearance *appearance = [self buildAppearance:vc withConfig:config]; navitem.standardAppearance = appearance; navitem.compactAppearance = appearance; UINavigationBarAppearance *scrollEdgeAppearance = [[UINavigationBarAppearance alloc] initWithBarAppearance:appearance]; if (config.largeTitleBackgroundColor != nil) { scrollEdgeAppearance.backgroundColor = config.largeTitleBackgroundColor; } if (config.largeTitleHideShadow) { scrollEdgeAppearance.shadowColor = nil; } navitem.scrollEdgeAppearance = scrollEdgeAppearance; } else #endif { #if !TARGET_OS_TV // updating backIndicatotImage does not work when called during transition. On iOS pre 13 we need // to update it before the navigation starts. UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config]; if (backButtonImage) { navctr.navigationBar.backIndicatorImage = backButtonImage; navctr.navigationBar.backIndicatorTransitionMaskImage = backButtonImage; } else if (navctr.navigationBar.backIndicatorImage) { navctr.navigationBar.backIndicatorImage = nil; navctr.navigationBar.backIndicatorTransitionMaskImage = nil; } #endif } #if !TARGET_OS_TV navitem.hidesBackButton = config.hideBackButton; #endif navitem.leftBarButtonItem = nil; navitem.rightBarButtonItem = nil; navitem.titleView = nil; for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) { switch (subview.type) { case RNSScreenStackHeaderSubviewTypeLeft: { #if !TARGET_OS_TV navitem.leftItemsSupplementBackButton = config.backButtonInCustomView; #endif UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithCustomView:subview]; navitem.leftBarButtonItem = buttonItem; break; } case RNSScreenStackHeaderSubviewTypeRight: { UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithCustomView:subview]; navitem.rightBarButtonItem = buttonItem; break; } case RNSScreenStackHeaderSubviewTypeCenter: case RNSScreenStackHeaderSubviewTypeTitle: { navitem.titleView = subview; break; } case RNSScreenStackHeaderSubviewTypeSearchBar: { if (subview.subviews == nil || [subview.subviews count] == 0) { RCTLogWarn( @"Failed to attach search bar to the header. We recommend using `useLayoutEffect` when managing " "searchBar properties dynamically. \n\nSee: github.com/software-mansion/react-native-screens/issues/1188"); break; } if ([subview.subviews[0] isKindOfClass:[RNSSearchBar class]]) { #if !TARGET_OS_TV if (@available(iOS 11.0, *)) { RNSSearchBar *searchBar = subview.subviews[0]; navitem.searchController = searchBar.controller; navitem.hidesSearchBarWhenScrolling = searchBar.hideWhenScrolling; } #endif } break; } case RNSScreenStackHeaderSubviewTypeBackButton: { #ifdef RN_FABRIC_ENABLED RCTLogWarn(@"Back button subview is not yet Fabric compatible in react-native-screens"); #endif break; } } } // 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; if (animated && vc.transitionCoordinator != nil && vc.transitionCoordinator.presentationStyle == UIModalPresentationNone && !wasHidden) { // when there is an ongoing transition we may need to update navbar setting in animation block // using animateAlongsideTransition. However, we only do that given the transition is not a modal // transition (presentationStyle == UIModalPresentationNone) and that the bar was not previously // hidden. This is because both for modal transitions and transitions from screen with hidden bar // the transition animation block does not get triggered. This is ok, because with both of those // types of transitions there is no "shared" navigation bar that needs to be updated in an animated // way. [vc.transitionCoordinator animateAlongsideTransition:^(id _Nonnull context) { [self setAnimatedConfig:vc withConfig:config]; } completion:^(id _Nonnull context) { if ([context isCancelled]) { UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey]; RNSScreenStackHeaderConfig *config = nil; for (UIView *subview in fromVC.view.reactSubviews) { if ([subview isKindOfClass:[RNSScreenStackHeaderConfig class]]) { config = (RNSScreenStackHeaderConfig *)subview; break; } } [self setAnimatedConfig:fromVC withConfig:config]; } }]; } else { [self setAnimatedConfig:vc withConfig:config]; } } - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex { [_reactSubviews insertObject:subview atIndex:atIndex]; subview.reactSuperview = self; } - (void)removeReactSubview:(RNSScreenStackHeaderSubview *)subview { [_reactSubviews removeObject:subview]; } - (void)didUpdateReactSubviews { [super didUpdateReactSubviews]; [self updateViewControllerIfNeeded]; } #ifdef RN_FABRIC_ENABLED #pragma mark - Fabric specific - (void)mountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { if (![childComponentView isKindOfClass:[RNSScreenStackHeaderSubview class]]) { RCTLogError(@"ScreenStackHeader only accepts children of type ScreenStackHeaderSubview"); return; } RCTAssert( childComponentView.superview == nil, @"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)", self, childComponentView, @(index), @([childComponentView.superview tag])); // [_reactSubviews insertObject:(RNSScreenStackHeaderSubview *)childComponentView atIndex:index]; [self insertReactSubview:(RNSScreenStackHeaderSubview *)childComponentView atIndex:index]; [self updateViewControllerIfNeeded]; } - (void)unmountChildComponentView:(UIView *)childComponentView index:(NSInteger)index { [_reactSubviews removeObject:(RNSScreenStackHeaderSubview *)childComponentView]; [childComponentView removeFromSuperview]; } #pragma mark - RCTComponentViewProtocol - (void)prepareForRecycle { [super prepareForRecycle]; _initialPropsSet = NO; } + (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider { return facebook::react::concreteComponentDescriptorProvider< facebook::react::RNSScreenStackHeaderConfigComponentDescriptor>(); } - (NSNumber *)getFontSizePropValue:(int)value { if (value > 0) return [NSNumber numberWithInt:value]; return nil; } - (UISemanticContentAttribute)getDirectionPropValue:(facebook::react::RNSScreenStackHeaderConfigDirection)direction { switch (direction) { case facebook::react::RNSScreenStackHeaderConfigDirection::Rtl: return UISemanticContentAttributeForceRightToLeft; case facebook::react::RNSScreenStackHeaderConfigDirection::Ltr: return UISemanticContentAttributeForceLeftToRight; } } - (void)updateProps:(facebook::react::Props::Shared const &)props oldProps:(facebook::react::Props::Shared const &)oldProps { [super updateProps:props oldProps:oldProps]; const auto &oldScreenProps = *std::static_pointer_cast(_props); const auto &newScreenProps = *std::static_pointer_cast(props); BOOL needsNavigationControllerLayout = !_initialPropsSet; if (newScreenProps.hidden != !_show) { _show = !newScreenProps.hidden; needsNavigationControllerLayout = YES; } if (newScreenProps.translucent != _translucent) { _translucent = newScreenProps.translucent; needsNavigationControllerLayout = YES; } if (newScreenProps.backButtonInCustomView != _backButtonInCustomView) { [self setBackButtonInCustomView:newScreenProps.backButtonInCustomView]; } _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); } _titleFontWeight = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontWeight); _titleFontSize = [self getFontSizePropValue:newScreenProps.titleFontSize]; _hideShadow = newScreenProps.hideShadow; _largeTitle = newScreenProps.largeTitle; if (newScreenProps.largeTitleFontFamily != oldScreenProps.largeTitleFontFamily) { _largeTitleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.largeTitleFontFamily); } _largeTitleFontWeight = RCTNSStringFromStringNilIfEmpty(newScreenProps.largeTitleFontWeight); _largeTitleFontSize = [self getFontSizePropValue:newScreenProps.largeTitleFontSize]; _largeTitleHideShadow = newScreenProps.largeTitleHideShadow; _backTitle = RCTNSStringFromStringNilIfEmpty(newScreenProps.backTitle); if (newScreenProps.backTitleFontFamily != oldScreenProps.backTitleFontFamily) { _backTitleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.backTitleFontFamily); } _backTitleFontSize = [self getFontSizePropValue:newScreenProps.backTitleFontSize]; _hideBackButton = newScreenProps.hideBackButton; _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; if (newScreenProps.direction != oldScreenProps.direction) { _direction = [self getDirectionPropValue:newScreenProps.direction]; } // We cannot compare SharedColor because it is shared value. // We could compare color value, but it is more performant to just assign new value _titleColor = RCTUIColorFromSharedColor(newScreenProps.titleColor); _largeTitleColor = RCTUIColorFromSharedColor(newScreenProps.largeTitleColor); _color = RCTUIColorFromSharedColor(newScreenProps.color); _backgroundColor = RCTUIColorFromSharedColor(newScreenProps.backgroundColor); [self updateViewControllerIfNeeded]; if (needsNavigationControllerLayout) { [self layoutNavigationControllerView]; } _initialPropsSet = YES; _props = std::static_pointer_cast(props); } #else #pragma mark - Paper specific - (void)didSetProps:(NSArray *)changedProps { [super didSetProps:changedProps]; [self updateViewControllerIfNeeded]; // We need to layout navigation controller view after translucent prop changes, because otherwise // frame of RNSScreen will not be changed and screen content will remain the same size. // For more details look at https://github.com/software-mansion/react-native-screens/issues/1158 if ([changedProps containsObject:@"translucent"]) { [self layoutNavigationControllerView]; } } #endif @end #ifdef RN_FABRIC_ENABLED Class RNSScreenStackHeaderConfigCls(void) { return RNSScreenStackHeaderConfig.class; } #endif @implementation RNSScreenStackHeaderConfigManager RCT_EXPORT_MODULE() - (UIView *)view { return [RNSScreenStackHeaderConfig new]; } RCT_EXPORT_VIEW_PROPERTY(title, NSString) RCT_EXPORT_VIEW_PROPERTY(titleFontFamily, NSString) RCT_EXPORT_VIEW_PROPERTY(titleFontSize, NSNumber) RCT_EXPORT_VIEW_PROPERTY(titleFontWeight, NSString) RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(backTitle, NSString) RCT_EXPORT_VIEW_PROPERTY(backTitleFontFamily, NSString) RCT_EXPORT_VIEW_PROPERTY(backTitleFontSize, NSNumber) RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(blurEffect, UIBlurEffectStyle) RCT_EXPORT_VIEW_PROPERTY(color, UIColor) RCT_EXPORT_VIEW_PROPERTY(direction, UISemanticContentAttribute) RCT_EXPORT_VIEW_PROPERTY(largeTitle, BOOL) RCT_EXPORT_VIEW_PROPERTY(largeTitleFontFamily, NSString) RCT_EXPORT_VIEW_PROPERTY(largeTitleFontSize, NSNumber) RCT_EXPORT_VIEW_PROPERTY(largeTitleFontWeight, NSString) RCT_EXPORT_VIEW_PROPERTY(largeTitleColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(largeTitleBackgroundColor, UIColor) RCT_EXPORT_VIEW_PROPERTY(largeTitleHideShadow, BOOL) RCT_EXPORT_VIEW_PROPERTY(hideBackButton, BOOL) RCT_EXPORT_VIEW_PROPERTY(hideShadow, BOOL) RCT_EXPORT_VIEW_PROPERTY(backButtonInCustomView, BOOL) RCT_EXPORT_VIEW_PROPERTY(disableBackButtonMenu, BOOL) // `hidden` is an UIView property, we need to use different name internally RCT_REMAP_VIEW_PROPERTY(hidden, hide, BOOL) RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL) @end @implementation RCTConvert (RNSScreenStackHeader) + (NSMutableDictionary *)blurEffectsForIOSVersion { NSMutableDictionary *blurEffects = [NSMutableDictionary new]; [blurEffects addEntriesFromDictionary:@{ @"extraLight" : @(UIBlurEffectStyleExtraLight), @"light" : @(UIBlurEffectStyleLight), @"dark" : @(UIBlurEffectStyleDark), }]; if (@available(iOS 10.0, *)) { [blurEffects addEntriesFromDictionary:@{ @"regular" : @(UIBlurEffectStyleRegular), @"prominent" : @(UIBlurEffectStyleProminent), }]; } #if !TARGET_OS_TV && defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 if (@available(iOS 13.0, *)) { [blurEffects addEntriesFromDictionary:@{ @"systemUltraThinMaterial" : @(UIBlurEffectStyleSystemUltraThinMaterial), @"systemThinMaterial" : @(UIBlurEffectStyleSystemThinMaterial), @"systemMaterial" : @(UIBlurEffectStyleSystemMaterial), @"systemThickMaterial" : @(UIBlurEffectStyleSystemThickMaterial), @"systemChromeMaterial" : @(UIBlurEffectStyleSystemChromeMaterial), @"systemUltraThinMaterialLight" : @(UIBlurEffectStyleSystemUltraThinMaterialLight), @"systemThinMaterialLight" : @(UIBlurEffectStyleSystemThinMaterialLight), @"systemMaterialLight" : @(UIBlurEffectStyleSystemMaterialLight), @"systemThickMaterialLight" : @(UIBlurEffectStyleSystemThickMaterialLight), @"systemChromeMaterialLight" : @(UIBlurEffectStyleSystemChromeMaterialLight), @"systemUltraThinMaterialDark" : @(UIBlurEffectStyleSystemUltraThinMaterialDark), @"systemThinMaterialDark" : @(UIBlurEffectStyleSystemThinMaterialDark), @"systemMaterialDark" : @(UIBlurEffectStyleSystemMaterialDark), @"systemThickMaterialDark" : @(UIBlurEffectStyleSystemThickMaterialDark), @"systemChromeMaterialDark" : @(UIBlurEffectStyleSystemChromeMaterialDark), }]; } #endif return blurEffects; } RCT_ENUM_CONVERTER( UISemanticContentAttribute, (@{ @"ltr" : @(UISemanticContentAttributeForceLeftToRight), @"rtl" : @(UISemanticContentAttributeForceRightToLeft), }), UISemanticContentAttributeUnspecified, integerValue) RCT_ENUM_CONVERTER(UIBlurEffectStyle, ([self blurEffectsForIOSVersion]), UIBlurEffectStyleExtraLight, integerValue) @end