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