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