1#ifdef RN_FABRIC_ENABLED 2#import <React/RCTConversions.h> 3#import <React/RCTFabricComponentsPlugins.h> 4#import <React/UIView+React.h> 5#import <react/renderer/components/rnscreens/ComponentDescriptors.h> 6#import <react/renderer/components/rnscreens/EventEmitters.h> 7#import <react/renderer/components/rnscreens/Props.h> 8#import <react/renderer/components/rnscreens/RCTComponentViewHelpers.h> 9#else 10#import <React/RCTBridge.h> 11#import <React/RCTImageLoader.h> 12#import <React/RCTImageSource.h> 13#import <React/RCTImageView.h> 14#import <React/RCTShadowView.h> 15#import <React/RCTUIManager.h> 16#import <React/RCTUIManagerUtils.h> 17#endif 18#import <React/RCTFont.h> 19#import "RNSScreen.h" 20#import "RNSScreenStackHeaderConfig.h" 21#import "RNSSearchBar.h" 22#import "RNSUIBarButtonItem.h" 23 24#ifdef RN_FABRIC_ENABLED 25#else 26// Some RN private method hacking below. Couldn't figure out better way to access image data 27// of a given RCTImageView. See more comments in the code section processing SubviewTypeBackButton 28@interface RCTImageView (Private) 29- (UIImage *)image; 30@end 31 32@interface RCTImageLoader (Private) 33- (id<RCTImageCache>)imageCache; 34@end 35#endif 36 37@implementation RNSScreenStackHeaderConfig { 38 NSMutableArray<RNSScreenStackHeaderSubview *> *_reactSubviews; 39#ifdef RN_FABRIC_ENABLED 40 BOOL _initialPropsSet; 41#else 42#endif 43} 44 45#ifdef RN_FABRIC_ENABLED 46- (instancetype)initWithFrame:(CGRect)frame 47{ 48 if (self = [super initWithFrame:frame]) { 49 static const auto defaultProps = std::make_shared<const facebook::react::RNSScreenStackHeaderConfigProps>(); 50 _props = defaultProps; 51 self.hidden = YES; 52 _show = YES; 53 _translucent = NO; 54 _reactSubviews = [NSMutableArray new]; 55 } 56 return self; 57} 58#else 59- (instancetype)init 60{ 61 if (self = [super init]) { 62 self.hidden = YES; 63 _translucent = YES; 64 _reactSubviews = [NSMutableArray new]; 65 } 66 return self; 67} 68#endif 69 70- (UIView *)reactSuperview 71{ 72 return _screenView; 73} 74 75- (NSArray<UIView *> *)reactSubviews 76{ 77 return _reactSubviews; 78} 79 80- (void)removeFromSuperview 81{ 82 [super removeFromSuperview]; 83 _screenView = nil; 84} 85 86// this method is never invoked by the system since this view 87// is not added to native view hierarchy so we can apply our logic 88- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event 89{ 90 for (RNSScreenStackHeaderSubview *subview in _reactSubviews) { 91 if (subview.type == RNSScreenStackHeaderSubviewTypeLeft || subview.type == RNSScreenStackHeaderSubviewTypeRight) { 92 // we wrap the headerLeft/Right component in a UIBarButtonItem 93 // so we need to use the only subview of it to retrieve the correct view 94 UIView *headerComponent = subview.subviews.firstObject; 95 // we convert the point to RNSScreenStackView since it always contains the header inside it 96 CGPoint convertedPoint = [_screenView.reactSuperview convertPoint:point toView:headerComponent]; 97 98 UIView *hitTestResult = [headerComponent hitTest:convertedPoint withEvent:event]; 99 if (hitTestResult != nil) { 100 return hitTestResult; 101 } 102 } 103 } 104 return nil; 105} 106 107- (void)updateViewControllerIfNeeded 108{ 109 UIViewController *vc = _screenView.controller; 110 UINavigationController *nav = (UINavigationController *)vc.parentViewController; 111 UIViewController *nextVC = nav.visibleViewController; 112 if (nav.transitionCoordinator != nil) { 113 // if navigator is performing transition instead of allowing to update of `visibleConttroller` 114 // we look at `topController`. This is because during transitiong the `visibleController` won't 115 // point to the controller that is going to be revealed after transition. This check fixes the 116 // problem when config gets updated while the transition is ongoing. 117 nextVC = nav.topViewController; 118 } 119 120 // we want updates sent to the VC below modal too since it is also visible 121 BOOL isPresentingVC = nextVC != nil && vc.presentedViewController == nextVC; 122 123 BOOL isInFullScreenModal = nav == nil && _screenView.stackPresentation == RNSScreenStackPresentationFullScreenModal; 124 // if nav is nil, it means we can be in a fullScreen modal, so there is no nextVC, but we still want to update 125 if (vc != nil && (nextVC == vc || isInFullScreenModal || isPresentingVC)) { 126 [RNSScreenStackHeaderConfig updateViewController:self.screenView.controller withConfig:self animated:YES]; 127 } 128} 129 130- (void)layoutNavigationControllerView 131{ 132 // We need to layout navigation controller view after translucent prop changes, because otherwise 133 // frame of RNSScreen will not be changed and screen content will remain the same size. 134 // For more details look at https://github.com/software-mansion/react-native-screens/issues/1158 135 UIViewController *vc = _screenView.controller; 136 UINavigationController *navctr = vc.navigationController; 137 [navctr.view setNeedsLayout]; 138} 139 140+ (void)setAnimatedConfig:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config 141{ 142 UINavigationBar *navbar = ((UINavigationController *)vc.parentViewController).navigationBar; 143 // It is workaround for loading custom back icon when transitioning from a screen without header to the screen which 144 // has one. This action fails when navigating to the screen with header for the second time and loads default back 145 // button. It looks like changing the tint color of navbar triggers an update of the items belonging to it and it 146 // 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 147 // the one it should have. 148#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_14_0) && \ 149 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_14_0 150 // it brakes the behavior of `headerRight` in iOS 14, where the bug desribed above seems to be fixed, so we do nothing 151 // in iOS 14 152 if (@available(iOS 14.0, *)) { 153 } else 154#endif 155 { 156 [navbar setTintColor:[config.color colorWithAlphaComponent:CGColorGetAlpha(config.color.CGColor) - 0.01]]; 157 } 158 [navbar setTintColor:config.color]; 159 160#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ 161 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 162 if (@available(iOS 13.0, *)) { 163 // font customized on the navigation item level, so nothing to do here 164 } else 165#endif 166 { 167 BOOL hideShadow = config.hideShadow; 168 169 if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) { 170 [navbar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; 171 [navbar setBarTintColor:[UIColor clearColor]]; 172 hideShadow = YES; 173 } else { 174 [navbar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; 175 [navbar setBarTintColor:config.backgroundColor]; 176 } 177 [navbar setTranslucent:config.translucent]; 178 [navbar setValue:@(hideShadow ? YES : NO) forKey:@"hidesShadow"]; 179 180 if (config.titleFontFamily || config.titleFontSize || config.titleFontWeight || config.titleColor) { 181 NSMutableDictionary *attrs = [NSMutableDictionary new]; 182 183 if (config.titleColor) { 184 attrs[NSForegroundColorAttributeName] = config.titleColor; 185 } 186 187 NSString *family = config.titleFontFamily ?: nil; 188 NSNumber *size = config.titleFontSize ?: @17; 189 NSString *weight = config.titleFontWeight ?: nil; 190 if (family || weight) { 191 attrs[NSFontAttributeName] = [RCTFont updateFont:nil 192 withFamily:family 193 size:size 194 weight:weight 195 style:nil 196 variant:nil 197 scaleMultiplier:1.0]; 198 } else { 199 attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:[size floatValue]]; 200 } 201 [navbar setTitleTextAttributes:attrs]; 202 } 203 204#if !TARGET_OS_TV 205 if (@available(iOS 11.0, *)) { 206 if (config.largeTitle && 207 (config.largeTitleFontFamily || config.largeTitleFontSize || config.largeTitleFontWeight || 208 config.largeTitleColor || config.titleColor)) { 209 NSMutableDictionary *largeAttrs = [NSMutableDictionary new]; 210 if (config.largeTitleColor || config.titleColor) { 211 largeAttrs[NSForegroundColorAttributeName] = 212 config.largeTitleColor ? config.largeTitleColor : config.titleColor; 213 } 214 NSString *largeFamily = config.largeTitleFontFamily ?: nil; 215 NSNumber *largeSize = config.largeTitleFontSize ?: @34; 216 NSString *largeWeight = config.largeTitleFontWeight ?: nil; 217 if (largeFamily || largeWeight) { 218 largeAttrs[NSFontAttributeName] = [RCTFont updateFont:nil 219 withFamily:largeFamily 220 size:largeSize 221 weight:largeWeight 222 style:nil 223 variant:nil 224 scaleMultiplier:1.0]; 225 } else { 226 largeAttrs[NSFontAttributeName] = [UIFont systemFontOfSize:[largeSize floatValue] weight:UIFontWeightBold]; 227 } 228 [navbar setLargeTitleTextAttributes:largeAttrs]; 229 } 230 } 231#endif 232 } 233} 234 235+ (void)setTitleAttibutes:(NSDictionary *)attrs forButton:(UIBarButtonItem *)button 236{ 237 [button setTitleTextAttributes:attrs forState:UIControlStateNormal]; 238 [button setTitleTextAttributes:attrs forState:UIControlStateHighlighted]; 239 [button setTitleTextAttributes:attrs forState:UIControlStateDisabled]; 240 [button setTitleTextAttributes:attrs forState:UIControlStateSelected]; 241 [button setTitleTextAttributes:attrs forState:UIControlStateFocused]; 242} 243 244+ (UIImage *)loadBackButtonImageInViewController:(UIViewController *)vc withConfig:(RNSScreenStackHeaderConfig *)config 245{ 246#ifdef RN_FABRIC_ENABLED 247 @throw([NSException exceptionWithName:@"UNIMPLEMENTED" reason:@"Implement" userInfo:nil]); 248#else 249 BOOL hasBackButtonImage = NO; 250 for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) { 251 if (subview.type == RNSScreenStackHeaderSubviewTypeBackButton && subview.subviews.count > 0) { 252 hasBackButtonImage = YES; 253 RCTImageView *imageView = subview.subviews[0]; 254 if (imageView.image == nil) { 255 // This is yet another workaround for loading custom back icon. It turns out that under 256 // certain circumstances image attribute can be null despite the app running in production 257 // mode (when images are loaded from the filesystem). This can happen because image attribute 258 // is reset when image view is detached from window, and also in some cases initialization 259 // does not populate the frame of the image view before the loading start. The latter result 260 // in the image attribute not being updated. We manually set frame to the size of an image 261 // in order to trigger proper reload that'd update the image attribute. 262 RCTImageSource *source = imageView.imageSources[0]; 263 [imageView reactSetFrame:CGRectMake( 264 imageView.frame.origin.x, 265 imageView.frame.origin.y, 266 source.size.width, 267 source.size.height)]; 268 } 269 UIImage *image = imageView.image; 270 // IMPORTANT!!! 271 // image can be nil in DEV MODE ONLY 272 // 273 // It is so, because in dev mode images are loaded over HTTP from the packager. In that case 274 // we first check if image is already loaded in cache and if it is, we take it from cache and 275 // display immediately. Otherwise we wait for the transition to finish and retry updating 276 // header config. 277 // Unfortunately due to some problems in UIKit we cannot update the image while the screen 278 // transition is ongoing. This results in the settings being reset after the transition is done 279 // to the state from before the transition. 280 if (image == nil) { 281 // in DEV MODE we try to load from cache (we use private API for that as it is not exposed 282 // publically in headers). 283 RCTImageSource *source = imageView.imageSources[0]; 284 RCTImageLoader *imageloader = [subview.bridge moduleForClass:[RCTImageLoader class]]; 285 image = [imageloader.imageCache imageForUrl:source.request.URL.absoluteString 286 size:source.size 287 scale:source.scale 288 resizeMode:imageView.resizeMode]; 289 } 290 if (image == nil) { 291 // This will be triggered if the image is not in the cache yet. What we do is we wait until 292 // the end of transition and run header config updates again. We could potentially wait for 293 // image on load to trigger, but that would require even more private method hacking. 294 if (vc.transitionCoordinator) { 295 [vc.transitionCoordinator 296 animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { 297 // nothing, we just want completion 298 } 299 completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { 300 // in order for new back button image to be loaded we need to trigger another change 301 // in back button props that'd make UIKit redraw the button. Otherwise the changes are 302 // not reflected. Here we change back button visibility which is then immediately restored 303#if !TARGET_OS_TV 304 vc.navigationItem.hidesBackButton = YES; 305#endif 306 [config updateViewControllerIfNeeded]; 307 }]; 308 } 309 return [UIImage new]; 310 } else { 311 return image; 312 } 313 } 314 } 315#endif // RN_FABRIC_ENABLED 316 return nil; 317} 318 319+ (void)willShowViewController:(UIViewController *)vc 320 animated:(BOOL)animated 321 withConfig:(RNSScreenStackHeaderConfig *)config 322{ 323 [self updateViewController:vc withConfig:config animated:animated]; 324} 325 326#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ 327 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 328+ (UINavigationBarAppearance *)buildAppearance:(UIViewController *)vc 329 withConfig:(RNSScreenStackHeaderConfig *)config API_AVAILABLE(ios(13.0)) 330{ 331 UINavigationBarAppearance *appearance = [UINavigationBarAppearance new]; 332 333 if (config.backgroundColor && CGColorGetAlpha(config.backgroundColor.CGColor) == 0.) { 334 // transparent background color 335 [appearance configureWithTransparentBackground]; 336 } else { 337 [appearance configureWithOpaqueBackground]; 338 } 339 340 // set background color if specified 341 if (config.backgroundColor) { 342 appearance.backgroundColor = config.backgroundColor; 343 } 344 345 // TODO: implement blurEffect on Fabric 346#ifdef RN_FABRIC_ENABLED 347#else 348 if (config.blurEffect) { 349 appearance.backgroundEffect = [UIBlurEffect effectWithStyle:config.blurEffect]; 350 } 351#endif 352 353 if (config.hideShadow) { 354 appearance.shadowColor = nil; 355 } 356 357 if (config.titleFontFamily || config.titleFontSize || config.titleFontWeight || config.titleColor) { 358 NSMutableDictionary *attrs = [NSMutableDictionary new]; 359 360 if (config.titleColor) { 361 attrs[NSForegroundColorAttributeName] = config.titleColor; 362 } 363 364 NSString *family = config.titleFontFamily ?: nil; 365 NSNumber *size = config.titleFontSize ?: @17; 366 NSString *weight = config.titleFontWeight ?: nil; 367 if (family || weight) { 368 attrs[NSFontAttributeName] = [RCTFont updateFont:nil 369 withFamily:config.titleFontFamily 370 size:size 371 weight:weight 372 style:nil 373 variant:nil 374 scaleMultiplier:1.0]; 375 } else { 376 attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:[size floatValue]]; 377 } 378 appearance.titleTextAttributes = attrs; 379 } 380 381 if (config.largeTitleFontFamily || config.largeTitleFontSize || config.largeTitleFontWeight || 382 config.largeTitleColor || config.titleColor) { 383 NSMutableDictionary *largeAttrs = [NSMutableDictionary new]; 384 385 if (config.largeTitleColor || config.titleColor) { 386 largeAttrs[NSForegroundColorAttributeName] = config.largeTitleColor ? config.largeTitleColor : config.titleColor; 387 } 388 389 NSString *largeFamily = config.largeTitleFontFamily ?: nil; 390 NSNumber *largeSize = config.largeTitleFontSize ?: @34; 391 NSString *largeWeight = config.largeTitleFontWeight ?: nil; 392 if (largeFamily || largeWeight) { 393 largeAttrs[NSFontAttributeName] = [RCTFont updateFont:nil 394 withFamily:largeFamily 395 size:largeSize 396 weight:largeWeight 397 style:nil 398 variant:nil 399 scaleMultiplier:1.0]; 400 } else { 401 largeAttrs[NSFontAttributeName] = [UIFont systemFontOfSize:[largeSize floatValue] weight:UIFontWeightBold]; 402 } 403 404 appearance.largeTitleTextAttributes = largeAttrs; 405 } 406 407#ifdef RN_FABRIC_ENABLED 408 [appearance setBackIndicatorImage:nil transitionMaskImage:nil]; 409#else 410 UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config]; 411 if (backButtonImage) { 412 [appearance setBackIndicatorImage:backButtonImage transitionMaskImage:backButtonImage]; 413 } else if (appearance.backIndicatorImage) { 414 [appearance setBackIndicatorImage:nil transitionMaskImage:nil]; 415 } 416#endif // RN_FABRIC_ENABLED 417 return appearance; 418} 419#endif 420 421+ (void)updateViewController:(UIViewController *)vc 422 withConfig:(RNSScreenStackHeaderConfig *)config 423 animated:(BOOL)animated 424{ 425 UINavigationItem *navitem = vc.navigationItem; 426 UINavigationController *navctr = (UINavigationController *)vc.parentViewController; 427 428 NSUInteger currentIndex = [navctr.viewControllers indexOfObject:vc]; 429 UINavigationItem *prevItem = 430 currentIndex > 0 ? [navctr.viewControllers objectAtIndex:currentIndex - 1].navigationItem : nil; 431 432 BOOL wasHidden = navctr.navigationBarHidden; 433#ifdef RN_FABRIC_ENABLED 434 BOOL shouldHide = config == nil || !config.show; 435#else 436 BOOL shouldHide = config == nil || config.hide; 437#endif 438 439 if (!shouldHide && !config.translucent) { 440 // when nav bar is not translucent we chage edgesForExtendedLayout to avoid system laying out 441 // the screen underneath navigation controllers 442 vc.edgesForExtendedLayout = UIRectEdgeNone; 443 } else { 444 // system default is UIRectEdgeAll 445 vc.edgesForExtendedLayout = UIRectEdgeAll; 446 } 447 448 [navctr setNavigationBarHidden:shouldHide animated:animated]; 449 450 if ((config.direction == UISemanticContentAttributeForceLeftToRight || 451 config.direction == UISemanticContentAttributeForceRightToLeft) && 452 // iOS 12 cancels swipe gesture when direction is changed. See #1091 453 navctr.view.semanticContentAttribute != config.direction) { 454 navctr.view.semanticContentAttribute = config.direction; 455 navctr.navigationBar.semanticContentAttribute = config.direction; 456 } 457 458 if (shouldHide) { 459 return; 460 } 461 462#if !TARGET_OS_TV 463 // Fix for github.com/react-navigation/react-navigation/issues/11015 464 // It allows to hide back button title and use back button menu as normal. 465 // Back button display mode and back button menu are available since iOS 14. 466 if (@available(iOS 14.0, *)) { 467 // Make sure to set display mode to default. 468 // This line resets back button display mode - especially needed on the Fabric architecture. 469 navitem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeDefault; 470 471 NSString *trimmedBackTitle = 472 [config.backTitle stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; 473 474 // When an whitespace only back title is passed set back button mode to minimal. 475 if (config.backTitle != nil && [trimmedBackTitle length] == 0) { 476 navitem.backButtonDisplayMode = UINavigationItemBackButtonDisplayModeMinimal; 477 } 478 } else if ( 479 config.backTitle != nil || config.backTitleFontFamily || config.backTitleFontSize || 480 config.disableBackButtonMenu) { 481 RNSUIBarButtonItem *backBarButtonItem = [[RNSUIBarButtonItem alloc] initWithTitle:config.backTitle ?: prevItem.title 482 style:UIBarButtonItemStylePlain 483 target:nil 484 action:nil]; 485 486 [backBarButtonItem setMenuHidden:config.disableBackButtonMenu]; 487 488 prevItem.backBarButtonItem = backBarButtonItem; 489 if (config.backTitleFontFamily || config.backTitleFontSize) { 490 NSMutableDictionary *attrs = [NSMutableDictionary new]; 491 NSNumber *size = config.backTitleFontSize ?: @17; 492 if (config.backTitleFontFamily) { 493 attrs[NSFontAttributeName] = [RCTFont updateFont:nil 494 withFamily:config.backTitleFontFamily 495 size:size 496 weight:nil 497 style:nil 498 variant:nil 499 scaleMultiplier:1.0]; 500 } else { 501 attrs[NSFontAttributeName] = [UIFont boldSystemFontOfSize:[size floatValue]]; 502 } 503 [self setTitleAttibutes:attrs forButton:prevItem.backBarButtonItem]; 504 } 505 } else { 506 prevItem.backBarButtonItem = nil; 507 } 508 509 if (@available(iOS 11.0, *)) { 510 if (config.largeTitle) { 511 navctr.navigationBar.prefersLargeTitles = YES; 512 } 513 navitem.largeTitleDisplayMode = 514 config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever; 515 } 516#endif 517 518#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ 519 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 520 if (@available(iOS 13.0, tvOS 13.0, *)) { 521 UINavigationBarAppearance *appearance = [self buildAppearance:vc withConfig:config]; 522 navitem.standardAppearance = appearance; 523 navitem.compactAppearance = appearance; 524 525 UINavigationBarAppearance *scrollEdgeAppearance = 526 [[UINavigationBarAppearance alloc] initWithBarAppearance:appearance]; 527 if (config.largeTitleBackgroundColor != nil) { 528 scrollEdgeAppearance.backgroundColor = config.largeTitleBackgroundColor; 529 } 530 if (config.largeTitleHideShadow) { 531 scrollEdgeAppearance.shadowColor = nil; 532 } 533 navitem.scrollEdgeAppearance = scrollEdgeAppearance; 534 } else 535#endif 536 { 537#if !TARGET_OS_TV 538 // updating backIndicatotImage does not work when called during transition. On iOS pre 13 we need 539 // to update it before the navigation starts. 540 UIImage *backButtonImage = [self loadBackButtonImageInViewController:vc withConfig:config]; 541 if (backButtonImage) { 542 navctr.navigationBar.backIndicatorImage = backButtonImage; 543 navctr.navigationBar.backIndicatorTransitionMaskImage = backButtonImage; 544 } else if (navctr.navigationBar.backIndicatorImage) { 545 navctr.navigationBar.backIndicatorImage = nil; 546 navctr.navigationBar.backIndicatorTransitionMaskImage = nil; 547 } 548#endif 549 } 550#if !TARGET_OS_TV 551 navitem.hidesBackButton = config.hideBackButton; 552#endif 553 navitem.leftBarButtonItem = nil; 554 navitem.rightBarButtonItem = nil; 555 navitem.titleView = nil; 556 557 for (RNSScreenStackHeaderSubview *subview in config.reactSubviews) { 558 switch (subview.type) { 559 case RNSScreenStackHeaderSubviewTypeLeft: { 560#if !TARGET_OS_TV 561 navitem.leftItemsSupplementBackButton = config.backButtonInCustomView; 562#endif 563 UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithCustomView:subview]; 564 navitem.leftBarButtonItem = buttonItem; 565 break; 566 } 567 case RNSScreenStackHeaderSubviewTypeRight: { 568 UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithCustomView:subview]; 569 navitem.rightBarButtonItem = buttonItem; 570 break; 571 } 572 case RNSScreenStackHeaderSubviewTypeCenter: 573 case RNSScreenStackHeaderSubviewTypeTitle: { 574 navitem.titleView = subview; 575 break; 576 } 577 case RNSScreenStackHeaderSubviewTypeSearchBar: { 578 if (subview.subviews == nil || [subview.subviews count] == 0) { 579 RCTLogWarn( 580 @"Failed to attach search bar to the header. We recommend using `useLayoutEffect` when managing " 581 "searchBar properties dynamically. \n\nSee: github.com/software-mansion/react-native-screens/issues/1188"); 582 break; 583 } 584 585 if ([subview.subviews[0] isKindOfClass:[RNSSearchBar class]]) { 586#if !TARGET_OS_TV 587 if (@available(iOS 11.0, *)) { 588 RNSSearchBar *searchBar = subview.subviews[0]; 589 navitem.searchController = searchBar.controller; 590 navitem.hidesSearchBarWhenScrolling = searchBar.hideWhenScrolling; 591 } 592#endif 593 } 594 break; 595 } 596 case RNSScreenStackHeaderSubviewTypeBackButton: { 597#ifdef RN_FABRIC_ENABLED 598 RCTLogWarn(@"Back button subview is not yet Fabric compatible in react-native-screens"); 599#endif 600 break; 601 } 602 } 603 } 604 605 // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). 606 // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) 607 navitem.title = config.title; 608 609 if (animated && vc.transitionCoordinator != nil && 610 vc.transitionCoordinator.presentationStyle == UIModalPresentationNone && !wasHidden) { 611 // when there is an ongoing transition we may need to update navbar setting in animation block 612 // using animateAlongsideTransition. However, we only do that given the transition is not a modal 613 // transition (presentationStyle == UIModalPresentationNone) and that the bar was not previously 614 // hidden. This is because both for modal transitions and transitions from screen with hidden bar 615 // the transition animation block does not get triggered. This is ok, because with both of those 616 // types of transitions there is no "shared" navigation bar that needs to be updated in an animated 617 // way. 618 [vc.transitionCoordinator 619 animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { 620 [self setAnimatedConfig:vc withConfig:config]; 621 } 622 completion:^(id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) { 623 if ([context isCancelled]) { 624 UIViewController *fromVC = [context viewControllerForKey:UITransitionContextFromViewControllerKey]; 625 RNSScreenStackHeaderConfig *config = nil; 626 for (UIView *subview in fromVC.view.reactSubviews) { 627 if ([subview isKindOfClass:[RNSScreenStackHeaderConfig class]]) { 628 config = (RNSScreenStackHeaderConfig *)subview; 629 break; 630 } 631 } 632 [self setAnimatedConfig:fromVC withConfig:config]; 633 } 634 }]; 635 } else { 636 [self setAnimatedConfig:vc withConfig:config]; 637 } 638} 639 640- (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex 641{ 642 [_reactSubviews insertObject:subview atIndex:atIndex]; 643 subview.reactSuperview = self; 644} 645 646- (void)removeReactSubview:(RNSScreenStackHeaderSubview *)subview 647{ 648 [_reactSubviews removeObject:subview]; 649} 650 651- (void)didUpdateReactSubviews 652{ 653 [super didUpdateReactSubviews]; 654 [self updateViewControllerIfNeeded]; 655} 656 657#ifdef RN_FABRIC_ENABLED 658#pragma mark - Fabric specific 659 660- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index 661{ 662 if (![childComponentView isKindOfClass:[RNSScreenStackHeaderSubview class]]) { 663 RCTLogError(@"ScreenStackHeader only accepts children of type ScreenStackHeaderSubview"); 664 return; 665 } 666 667 RCTAssert( 668 childComponentView.superview == nil, 669 @"Attempt to mount already mounted component view. (parent: %@, child: %@, index: %@, existing parent: %@)", 670 self, 671 childComponentView, 672 @(index), 673 @([childComponentView.superview tag])); 674 675 // [_reactSubviews insertObject:(RNSScreenStackHeaderSubview *)childComponentView atIndex:index]; 676 [self insertReactSubview:(RNSScreenStackHeaderSubview *)childComponentView atIndex:index]; 677 [self updateViewControllerIfNeeded]; 678} 679 680- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index 681{ 682 [_reactSubviews removeObject:(RNSScreenStackHeaderSubview *)childComponentView]; 683 [childComponentView removeFromSuperview]; 684} 685 686#pragma mark - RCTComponentViewProtocol 687 688- (void)prepareForRecycle 689{ 690 [super prepareForRecycle]; 691 _initialPropsSet = NO; 692} 693 694+ (facebook::react::ComponentDescriptorProvider)componentDescriptorProvider 695{ 696 return facebook::react::concreteComponentDescriptorProvider< 697 facebook::react::RNSScreenStackHeaderConfigComponentDescriptor>(); 698} 699 700- (NSNumber *)getFontSizePropValue:(int)value 701{ 702 if (value > 0) 703 return [NSNumber numberWithInt:value]; 704 return nil; 705} 706 707- (UISemanticContentAttribute)getDirectionPropValue:(facebook::react::RNSScreenStackHeaderConfigDirection)direction 708{ 709 switch (direction) { 710 case facebook::react::RNSScreenStackHeaderConfigDirection::Rtl: 711 return UISemanticContentAttributeForceRightToLeft; 712 case facebook::react::RNSScreenStackHeaderConfigDirection::Ltr: 713 return UISemanticContentAttributeForceLeftToRight; 714 } 715} 716 717- (void)updateProps:(facebook::react::Props::Shared const &)props 718 oldProps:(facebook::react::Props::Shared const &)oldProps 719{ 720 [super updateProps:props oldProps:oldProps]; 721 722 const auto &oldScreenProps = 723 *std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigProps>(_props); 724 const auto &newScreenProps = *std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigProps>(props); 725 726 BOOL needsNavigationControllerLayout = !_initialPropsSet; 727 728 if (newScreenProps.hidden != !_show) { 729 _show = !newScreenProps.hidden; 730 needsNavigationControllerLayout = YES; 731 } 732 733 if (newScreenProps.translucent != _translucent) { 734 _translucent = newScreenProps.translucent; 735 needsNavigationControllerLayout = YES; 736 } 737 738 if (newScreenProps.backButtonInCustomView != _backButtonInCustomView) { 739 [self setBackButtonInCustomView:newScreenProps.backButtonInCustomView]; 740 } 741 742 _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); 743 if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { 744 _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); 745 } 746 _titleFontWeight = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontWeight); 747 _titleFontSize = [self getFontSizePropValue:newScreenProps.titleFontSize]; 748 _hideShadow = newScreenProps.hideShadow; 749 750 _largeTitle = newScreenProps.largeTitle; 751 if (newScreenProps.largeTitleFontFamily != oldScreenProps.largeTitleFontFamily) { 752 _largeTitleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.largeTitleFontFamily); 753 } 754 _largeTitleFontWeight = RCTNSStringFromStringNilIfEmpty(newScreenProps.largeTitleFontWeight); 755 _largeTitleFontSize = [self getFontSizePropValue:newScreenProps.largeTitleFontSize]; 756 _largeTitleHideShadow = newScreenProps.largeTitleHideShadow; 757 758 _backTitle = RCTNSStringFromStringNilIfEmpty(newScreenProps.backTitle); 759 if (newScreenProps.backTitleFontFamily != oldScreenProps.backTitleFontFamily) { 760 _backTitleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.backTitleFontFamily); 761 } 762 _backTitleFontSize = [self getFontSizePropValue:newScreenProps.backTitleFontSize]; 763 _hideBackButton = newScreenProps.hideBackButton; 764 _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; 765 766 if (newScreenProps.direction != oldScreenProps.direction) { 767 _direction = [self getDirectionPropValue:newScreenProps.direction]; 768 } 769 770 // We cannot compare SharedColor because it is shared value. 771 // We could compare color value, but it is more performant to just assign new value 772 _titleColor = RCTUIColorFromSharedColor(newScreenProps.titleColor); 773 _largeTitleColor = RCTUIColorFromSharedColor(newScreenProps.largeTitleColor); 774 _color = RCTUIColorFromSharedColor(newScreenProps.color); 775 _backgroundColor = RCTUIColorFromSharedColor(newScreenProps.backgroundColor); 776 777 [self updateViewControllerIfNeeded]; 778 779 if (needsNavigationControllerLayout) { 780 [self layoutNavigationControllerView]; 781 } 782 783 _initialPropsSet = YES; 784 _props = std::static_pointer_cast<facebook::react::RNSScreenStackHeaderConfigProps const>(props); 785} 786 787#else 788#pragma mark - Paper specific 789 790- (void)didSetProps:(NSArray<NSString *> *)changedProps 791{ 792 [super didSetProps:changedProps]; 793 [self updateViewControllerIfNeeded]; 794 // We need to layout navigation controller view after translucent prop changes, because otherwise 795 // frame of RNSScreen will not be changed and screen content will remain the same size. 796 // For more details look at https://github.com/software-mansion/react-native-screens/issues/1158 797 if ([changedProps containsObject:@"translucent"]) { 798 [self layoutNavigationControllerView]; 799 } 800} 801 802#endif 803@end 804 805#ifdef RN_FABRIC_ENABLED 806Class<RCTComponentViewProtocol> RNSScreenStackHeaderConfigCls(void) 807{ 808 return RNSScreenStackHeaderConfig.class; 809} 810#endif 811 812@implementation RNSScreenStackHeaderConfigManager 813 814RCT_EXPORT_MODULE() 815 816- (UIView *)view 817{ 818 return [RNSScreenStackHeaderConfig new]; 819} 820 821RCT_EXPORT_VIEW_PROPERTY(title, NSString) 822RCT_EXPORT_VIEW_PROPERTY(titleFontFamily, NSString) 823RCT_EXPORT_VIEW_PROPERTY(titleFontSize, NSNumber) 824RCT_EXPORT_VIEW_PROPERTY(titleFontWeight, NSString) 825RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor) 826RCT_EXPORT_VIEW_PROPERTY(backTitle, NSString) 827RCT_EXPORT_VIEW_PROPERTY(backTitleFontFamily, NSString) 828RCT_EXPORT_VIEW_PROPERTY(backTitleFontSize, NSNumber) 829RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor) 830RCT_EXPORT_VIEW_PROPERTY(blurEffect, UIBlurEffectStyle) 831RCT_EXPORT_VIEW_PROPERTY(color, UIColor) 832RCT_EXPORT_VIEW_PROPERTY(direction, UISemanticContentAttribute) 833RCT_EXPORT_VIEW_PROPERTY(largeTitle, BOOL) 834RCT_EXPORT_VIEW_PROPERTY(largeTitleFontFamily, NSString) 835RCT_EXPORT_VIEW_PROPERTY(largeTitleFontSize, NSNumber) 836RCT_EXPORT_VIEW_PROPERTY(largeTitleFontWeight, NSString) 837RCT_EXPORT_VIEW_PROPERTY(largeTitleColor, UIColor) 838RCT_EXPORT_VIEW_PROPERTY(largeTitleBackgroundColor, UIColor) 839RCT_EXPORT_VIEW_PROPERTY(largeTitleHideShadow, BOOL) 840RCT_EXPORT_VIEW_PROPERTY(hideBackButton, BOOL) 841RCT_EXPORT_VIEW_PROPERTY(hideShadow, BOOL) 842RCT_EXPORT_VIEW_PROPERTY(backButtonInCustomView, BOOL) 843RCT_EXPORT_VIEW_PROPERTY(disableBackButtonMenu, BOOL) 844// `hidden` is an UIView property, we need to use different name internally 845RCT_REMAP_VIEW_PROPERTY(hidden, hide, BOOL) 846RCT_EXPORT_VIEW_PROPERTY(translucent, BOOL) 847 848@end 849 850@implementation RCTConvert (RNSScreenStackHeader) 851 852+ (NSMutableDictionary *)blurEffectsForIOSVersion 853{ 854 NSMutableDictionary *blurEffects = [NSMutableDictionary new]; 855 [blurEffects addEntriesFromDictionary:@{ 856 @"extraLight" : @(UIBlurEffectStyleExtraLight), 857 @"light" : @(UIBlurEffectStyleLight), 858 @"dark" : @(UIBlurEffectStyleDark), 859 }]; 860 861 if (@available(iOS 10.0, *)) { 862 [blurEffects addEntriesFromDictionary:@{ 863 @"regular" : @(UIBlurEffectStyleRegular), 864 @"prominent" : @(UIBlurEffectStyleProminent), 865 }]; 866 } 867#if !TARGET_OS_TV && defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && defined(__IPHONE_13_0) && \ 868 __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_13_0 869 if (@available(iOS 13.0, *)) { 870 [blurEffects addEntriesFromDictionary:@{ 871 @"systemUltraThinMaterial" : @(UIBlurEffectStyleSystemUltraThinMaterial), 872 @"systemThinMaterial" : @(UIBlurEffectStyleSystemThinMaterial), 873 @"systemMaterial" : @(UIBlurEffectStyleSystemMaterial), 874 @"systemThickMaterial" : @(UIBlurEffectStyleSystemThickMaterial), 875 @"systemChromeMaterial" : @(UIBlurEffectStyleSystemChromeMaterial), 876 @"systemUltraThinMaterialLight" : @(UIBlurEffectStyleSystemUltraThinMaterialLight), 877 @"systemThinMaterialLight" : @(UIBlurEffectStyleSystemThinMaterialLight), 878 @"systemMaterialLight" : @(UIBlurEffectStyleSystemMaterialLight), 879 @"systemThickMaterialLight" : @(UIBlurEffectStyleSystemThickMaterialLight), 880 @"systemChromeMaterialLight" : @(UIBlurEffectStyleSystemChromeMaterialLight), 881 @"systemUltraThinMaterialDark" : @(UIBlurEffectStyleSystemUltraThinMaterialDark), 882 @"systemThinMaterialDark" : @(UIBlurEffectStyleSystemThinMaterialDark), 883 @"systemMaterialDark" : @(UIBlurEffectStyleSystemMaterialDark), 884 @"systemThickMaterialDark" : @(UIBlurEffectStyleSystemThickMaterialDark), 885 @"systemChromeMaterialDark" : @(UIBlurEffectStyleSystemChromeMaterialDark), 886 }]; 887 } 888#endif 889 return blurEffects; 890} 891 892RCT_ENUM_CONVERTER( 893 UISemanticContentAttribute, 894 (@{ 895 @"ltr" : @(UISemanticContentAttributeForceLeftToRight), 896 @"rtl" : @(UISemanticContentAttributeForceRightToLeft), 897 }), 898 UISemanticContentAttributeUnspecified, 899 integerValue) 900 901RCT_ENUM_CONVERTER(UIBlurEffectStyle, ([self blurEffectsForIOSVersion]), UIBlurEffectStyleExtraLight, integerValue) 902 903@end 904