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