UNPKG

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