diff --git a/ios/RNSBarButtonItem.h b/ios/RNSBarButtonItem.hindex ea5325ea8d17b1ddfa790ff8dab48ce83142d4d3..acd2fd7ceb7162ed300abc4d3c9f0c24f4d63898 100644--- a/ios/RNSBarButtonItem.h+++ b/ios/RNSBarButtonItem.h@@ -13,4 +13,8 @@ typedef void (^RNSBarButtonMenuItemAction)(NSString *menuId); menuAction:(RNSBarButtonMenuItemAction)menuAction imageLoader:(RCTImageLoader *)imageLoader; ++ (UIMenu *)initUIMenuWithDict:(NSDictionary<NSString *, id> *)dict+ menuAction:(RNSBarButtonMenuItemAction)menuAction+ imageLoader:(RCTImageLoader *)imageLoader;+ @enddiff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mmindex 0eb1f09dee82edd99e3e614938233db5cdeaebfe..73298f10807520970f625e166d7f5dd1338206f1 100644--- a/ios/RNSBarButtonItem.mm+++ b/ios/RNSBarButtonItem.mm@@ -110,6 +110,58 @@ - (instancetype)initWithConfig:(NSDictionary<NSString *, id> *)dict if (dict[@"accessibilityHint"]) { self.accessibilityHint = dict[@"accessibilityHint"]; }+#if !TARGET_OS_TV && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)+ NSNumber *glassEffectNum = dict[@"glassEffect"];+ if (@available(iOS 26.0, *)) {+ if ([glassEffectNum boolValue]) {+ UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration];+ configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule;+ UIBackgroundConfiguration *background = configuration.background;+ background.strokeWidth = 0.0;+ background.strokeColor = UIColor.clearColor;+ configuration.background = background;+ configuration.contentInsets = NSDirectionalEdgeInsetsZero;+ if (title != nil) {+ configuration.title = title;+ }++ UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil];+ button.translatesAutoresizingMaskIntoConstraints = NO;+ button.enabled = self.enabled;+ button.accessibilityLabel = self.accessibilityLabel;+ button.accessibilityHint = self.accessibilityHint;++ CGFloat resolvedWidth = width != nil ? width.doubleValue : 56.0;+ [button.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES;+ [button.heightAnchor constraintEqualToConstant:50.0].active = YES;++ [[self class] resolveImageFromConfig:dict+ imageLoader:imageLoader+ completionBlock:^(UIImage *img) {+ UIButtonConfiguration *updatedConfiguration = button.configuration;+ updatedConfiguration.image = img;+ button.configuration = updatedConfiguration;+ }];++ NSDictionary *menu = dict[@"menu"];+ if (menu) {+ button.menu = [[self class] initUIMenuWithDict:menu menuAction:menuAction imageLoader:imageLoader];+ button.showsMenuAsPrimaryAction = YES;+ } else {+ NSString *buttonId = dict[@"buttonId"];+ if (buttonId && action) {+ UIAction *pressAction = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull sender) {+ action(buttonId);+ }];+ [button addAction:pressAction forControlEvents:UIControlEventTouchUpInside];+ }+ }++ self.customView = button;+ return self;+ }+ }+#endif #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 if (@available(tvOS 17.0, *)) {diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.hindex 919b984edc9f91ee9ac26faf257d8a721e26457c..5bb0cd6736ed6bc51db57e2a9326f758f22c51d8 100644--- a/ios/RNSScreenStackHeaderConfig.h+++ b/ios/RNSScreenStackHeaderConfig.h@@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, retain) NSString *title;+@property (nonatomic, retain) NSString *subtitle;+@property (nonatomic, retain) NSString *largeSubtitle; @property (nonatomic, retain) NSString *titleFontFamily; @property (nonatomic, retain) NSNumber *titleFontSize; @property (nonatomic, retain) NSString *titleFontWeight;@@ -45,9 +47,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL backButtonInCustomView; @property (nonatomic) UISemanticContentAttribute direction; @property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode;+@property (nonatomic) NSInteger navigationItemStyle; @property (nonatomic) RNSBlurEffectStyle blurEffect; @property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerRightBarButtonItems; @property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerLeftBarButtonItems;+@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerCenterBarButtonItems;+@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerToolbarItems; @property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled; NS_ASSUME_NONNULL_ENDdiff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mmindex 5970e3e3a624b9498b8dedfc16831df03a274d0c..073a72a68688369f39e54b61d1381b8d652cbb83 100644--- a/ios/RNSScreenStackHeaderConfig.mm+++ b/ios/RNSScreenStackHeaderConfig.mm@@ -30,6 +30,20 @@ static const NSNumber *const DEFAULT_TITLE_FONT_SIZE = @17; static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; +static NSInteger navigationItemStyleFromCppEquivalent(+ react::RNSScreenStackHeaderConfigNavigationItemStyle navigationItemStyle)+{+ switch (navigationItemStyle) {+ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Browser:+ return UINavigationItemStyleBrowser;+ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Editor:+ return UINavigationItemStyleEditor;+ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Navigator:+ default:+ return UINavigationItemStyleNavigator;+ }+}+ @interface RCTImageLoader (Private) - (id<RCTImageCache>)imageCache; @end@@ -47,6 +61,9 @@ + (BOOL)rnscreens_isBlankOrNull:(NSString *)string @end @interface RNSScreenStackHeaderConfig () <RCTMountingTransactionObserving>+#if !TARGET_OS_TV+- (NSArray<UIBarButtonItemGroup *> *)barButtonItemGroupsFromItems:(NSArray<UIBarButtonItem *> *)items;+#endif @end @implementation RNSScreenStackHeaderConfig {@@ -81,6 +98,7 @@ - (void)initProps self.hidden = YES; _reactSubviews = [NSMutableArray new]; _backTitleVisible = YES;+ _navigationItemStyle = UINavigationItemStyleNavigator; _blurEffect = RNSBlurEffectStyleNone; } @@ -496,6 +514,10 @@ + (void)updateViewController:(UIViewController *)vc if (shouldHide) { navitem.title = config.title;+ if (@available(iOS 26.0, *)) {+ navitem.subtitle = config.subtitle;+ navitem.largeSubtitle = config.largeSubtitle;+ } // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items. [navctr setNavigationBarHidden:YES animated:animated];@@ -512,11 +534,19 @@ + (void)updateViewController:(UIViewController *)vc } navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever;++ if (@available(iOS 16.0, *)) {+ navitem.style = (UINavigationItemStyle)config.navigationItemStyle;+ } #endif UINavigationBarAppearance *appearance = [self buildAppearance:vc withConfig:config]; navitem.standardAppearance = appearance; navitem.compactAppearance = appearance;+ if (@available(iOS 26.0, *)) {+ navitem.subtitle = config.subtitle;+ navitem.largeSubtitle = config.largeSubtitle;+ } // appearance does not apply to the tvOS so we need to use lagacy customization #if TARGET_OS_TV@@ -637,10 +667,286 @@ + (void)updateViewController:(UIViewController *)vc // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) navitem.title = config.title;- navitem.leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems- withCurrentItems:navitem.leftBarButtonItems];- navitem.rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems- withCurrentItems:navitem.rightBarButtonItems];+ NSArray<UIBarButtonItem *> *leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems+ withCurrentItems:navitem.leftBarButtonItems+ navigationItem:navitem];+ NSArray<UIBarButtonItem *> *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems+ withCurrentItems:navitem.rightBarButtonItems+ navigationItem:navitem];+ NSArray<UIBarButtonItem *> *centerBarButtonItems = [config barButtonItemsFromConfigs:config.headerCenterBarButtonItems+ withCurrentItems:@[]+ navigationItem:navitem];+#if !TARGET_OS_TV+ if (@available(iOS 16.0, *)) {+ navitem.leadingItemGroups = [config barButtonItemGroupsFromItems:leftBarButtonItems];+ navitem.trailingItemGroups = [config barButtonItemGroupsFromItems:rightBarButtonItems];+ if (@available(iOS 26.0, *)) {+ navitem.centerItemGroups = [config barButtonItemGroupsFromItems:centerBarButtonItems];+ }+ navitem.leftBarButtonItems = nil;+ navitem.rightBarButtonItems = nil;+ } else {+ navitem.leftBarButtonItems = leftBarButtonItems;+ navitem.rightBarButtonItems = rightBarButtonItems;+ }+#else+ navitem.leftBarButtonItems = leftBarButtonItems;+ navitem.rightBarButtonItems = rightBarButtonItems;+#endif+ NSDictionary<NSString *, id> *mailSearchToolbarConfig = nil;+ for (NSDictionary<NSString *, id> *toolbarConfig in config.headerToolbarItems) {+ if (toolbarConfig[@"mailSearchToolbar"]) {+ mailSearchToolbarConfig = toolbarConfig;+ break;+ }+ }+ if (mailSearchToolbarConfig == nil) {+ for (NSDictionary<NSString *, id> *rightConfig in config.headerRightBarButtonItems) {+ if ([rightConfig[@"bottomMailSearchToolbar"] boolValue]) {+ mailSearchToolbarConfig = rightConfig;+ break;+ }+ }+ }++ static NSInteger const RNSMailSearchToolbarViewTag = 260628;+ UIView *chromeHostView = vc.view;+ UIView *existingMailSearchToolbar = [chromeHostView viewWithTag:RNSMailSearchToolbarViewTag];+ if (existingMailSearchToolbar == nil) {+ existingMailSearchToolbar = [vc.view viewWithTag:RNSMailSearchToolbarViewTag];+ }+ [existingMailSearchToolbar removeFromSuperview];++ if (mailSearchToolbarConfig != nil) {+#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)+ if (@available(iOS 26.0, *)) {+ CGFloat horizontalInset = 18.0;+ NSNumber *width = mailSearchToolbarConfig[@"width"];+ CGFloat hostWidth = chromeHostView.bounds.size.width;+ if (hostWidth <= 0.0) {+ hostWidth = vc.view.bounds.size.width;+ }+ if (hostWidth <= 0.0) {+ hostWidth = [UIScreen mainScreen].bounds.size.width;+ }+ CGFloat fallbackWidth = MIN(560.0, MAX(300.0, hostWidth - (horizontalInset * 2.0)));+ CGFloat resolvedWidth = width != nil ? width.doubleValue : fallbackWidth;+ if (hostWidth > 0.0) {+ resolvedWidth = MIN(resolvedWidth, MAX(260.0, hostWidth - 12.0));+ }++ CGFloat toolbarHeight = 48.0;+ CGFloat toolbarRadius = toolbarHeight / 2.0;+ CGFloat buttonSize = toolbarHeight;+ CGFloat sideButtonReserve = buttonSize + 8.0;+ CGFloat searchFieldHeight = 42.0;+ CGFloat toolbarBottomSpacing = 4.0;+ UIFont *searchFont = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle3];+ NSDictionary<NSAttributedStringKey, id> *placeholderAttributes = @{+ NSForegroundColorAttributeName : UIColor.secondaryLabelColor,+ NSFontAttributeName : searchFont,+ };+ UIKeyboardLayoutGuide *keyboardLayoutGuide = chromeHostView.keyboardLayoutGuide;++ UIView *toolbarHost = [[UIView alloc] init];+ toolbarHost.tag = RNSMailSearchToolbarViewTag;+ toolbarHost.translatesAutoresizingMaskIntoConstraints = NO;+ [chromeHostView addSubview:toolbarHost];+ // Keyboard avoidance is best-effort: on the iOS 27 beta the keyboard+ // layout guide no longer rests at the bottom safe-area edge while the+ // keyboard is hidden, which pushed the toolbar offscreen. The required+ // resting position is the safe area; the keyboard guide only pulls the+ // toolbar up when it actually tracks a visible keyboard.+ NSLayoutConstraint *keyboardAvoidConstraint =+ [toolbarHost.bottomAnchor constraintEqualToAnchor:keyboardLayoutGuide.topAnchor+ constant:-toolbarBottomSpacing];+ keyboardAvoidConstraint.priority = UILayoutPriorityDefaultHigh;+ NSLayoutConstraint *restingBottomConstraint =+ [toolbarHost.bottomAnchor constraintEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor+ constant:-toolbarBottomSpacing];+ restingBottomConstraint.priority = UILayoutPriorityDefaultLow;+ [NSLayoutConstraint activateConstraints:@[+ [toolbarHost.centerXAnchor constraintEqualToAnchor:chromeHostView.centerXAnchor],+ [toolbarHost.bottomAnchor constraintLessThanOrEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor+ constant:-toolbarBottomSpacing],+ keyboardAvoidConstraint,+ restingBottomConstraint,+ [toolbarHost.widthAnchor constraintEqualToConstant:resolvedWidth],+ [toolbarHost.heightAnchor constraintEqualToConstant:toolbarHeight],+ ]];+ [chromeHostView bringSubviewToFront:toolbarHost];++ UIGlassEffect *glassEffect = [UIGlassEffect effectWithStyle:UIGlassEffectStyleRegular];+ glassEffect.interactive = YES;+ UIVisualEffectView *glassView = [[UIVisualEffectView alloc] initWithEffect:glassEffect];+ glassView.clipsToBounds = YES;+ glassView.layer.cornerRadius = toolbarRadius;+ glassView.translatesAutoresizingMaskIntoConstraints = NO;+ [toolbarHost addSubview:glassView];+ BOOL hasFilterButton =+ mailSearchToolbarConfig[@"filterButtonId"] != nil || mailSearchToolbarConfig[@"filterMenu"] != nil;+ BOOL hasComposeButton =+ mailSearchToolbarConfig[@"composeButtonId"] != nil || mailSearchToolbarConfig[@"composeMenu"] != nil;+ CGFloat glassLeadingInset = hasFilterButton ? sideButtonReserve : 0.0;+ CGFloat glassTrailingInset = hasComposeButton ? -sideButtonReserve : 0.0;+ [NSLayoutConstraint activateConstraints:@[+ [glassView.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor constant:glassLeadingInset],+ [glassView.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor constant:glassTrailingInset],+ [glassView.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor],+ [glassView.heightAnchor constraintEqualToConstant:toolbarHeight],+ ]];++ void (^emitButtonPress)(NSString *) = ^(NSString *buttonId) {+ auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>(+ config->_eventEmitter);+ if (eventEmitter && buttonId) {+ eventEmitter->onPressHeaderBarButtonItem(+ facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonItem{+ .buttonId = std::string([buttonId UTF8String])});+ }+ };++ void (^emitMenuPress)(NSString *) = ^(NSString *menuId) {+ auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>(+ config->_eventEmitter);+ if (eventEmitter && menuId) {+ eventEmitter->onPressHeaderBarButtonMenuItem(+ facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonMenuItem{+ .menuId = std::string([menuId UTF8String])});+ }+ };++ UIButton *(^makeGlassButton)(NSString *, NSString *, NSDictionary *) =+ ^UIButton *(NSString *systemImageName, NSString *buttonId, NSDictionary *menuConfig) {+ UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration];+ configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule;+ configuration.image = [UIImage systemImageNamed:systemImageName ?: @"circle"];+ configuration.baseForegroundColor = UIColor.labelColor;+ UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil];+ if (menuConfig != nil) {+ button.menu = [RNSBarButtonItem initUIMenuWithDict:menuConfig+ menuAction:emitMenuPress+ imageLoader:config->_imageLoader];+ button.showsMenuAsPrimaryAction = YES;+ } else if (buttonId != nil) {+ UIAction *action = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) {+ emitButtonPress(buttonId);+ }];+ [button addAction:action forControlEvents:UIControlEventTouchUpInside];+ }+ return button;+ };++ BOOL useFallbackSearchField = [mailSearchToolbarConfig[@"useFallbackSearchField"] boolValue];+ UISearchBar *searchBar =+ !useFallbackSearchField && navitem.searchController != nil ? navitem.searchController.searchBar : nil;+ NSString *placeholder = mailSearchToolbarConfig[@"placeholder"];+ if (searchBar != nil) {+ if (placeholder != nil) {+ searchBar.placeholder = placeholder;+ }+ searchBar.hidden = NO;+ searchBar.alpha = 1.0;+ searchBar.userInteractionEnabled = YES;+ searchBar.searchBarStyle = UISearchBarStyleMinimal;+ searchBar.backgroundImage = [UIImage new];+ searchBar.searchTextField.hidden = NO;+ searchBar.searchTextField.alpha = 1.0;+ searchBar.searchTextField.backgroundColor = UIColor.clearColor;+ searchBar.searchTextField.font = searchFont;+ searchBar.searchTextField.adjustsFontForContentSizeCategory = YES;+ searchBar.searchTextField.textColor = UIColor.labelColor;+ searchBar.searchTextField.tintColor = UIColor.labelColor;+ if (placeholder != nil) {+ searchBar.searchTextField.attributedPlaceholder =+ [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes];+ }+ searchBar.translatesAutoresizingMaskIntoConstraints = NO;+ [glassView.contentView addSubview:searchBar];+ [NSLayoutConstraint activateConstraints:@[+ [searchBar.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:8.0],+ [searchBar.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-8.0],+ [searchBar.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor],+ [searchBar.heightAnchor constraintEqualToConstant:searchFieldHeight],+ ]];+ } else {+ UISearchTextField *searchField = [UISearchTextField new];+ if (placeholder != nil) {+ searchField.placeholder = placeholder;+ searchField.attributedPlaceholder =+ [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes];+ }+ NSString *searchTextChangeId = mailSearchToolbarConfig[@"searchTextChangeId"];+ if (searchTextChangeId != nil) {+ [searchField addAction:[UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) {+ UISearchTextField *field = (UISearchTextField *)action.sender;+ emitButtonPress([NSString stringWithFormat:@"%@:%@", searchTextChangeId, field.text ?: @""]);+ }]+ forControlEvents:UIControlEventEditingChanged];+ }+ searchField.borderStyle = UITextBorderStyleNone;+ searchField.font = searchFont;+ searchField.adjustsFontForContentSizeCategory = YES;+ searchField.textColor = UIColor.labelColor;+ searchField.tintColor = UIColor.labelColor;+ searchField.translatesAutoresizingMaskIntoConstraints = NO;+ [glassView.contentView addSubview:searchField];+ [NSLayoutConstraint activateConstraints:@[+ [searchField.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:14.0],+ [searchField.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-14.0],+ [searchField.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor],+ [searchField.heightAnchor constraintEqualToConstant:searchFieldHeight],+ ]];+ }++ if (hasFilterButton) {+ UIButton *filterButton = makeGlassButton(+ mailSearchToolbarConfig[@"filterSystemImageName"] ?: @"line.3.horizontal.decrease",+ mailSearchToolbarConfig[@"filterButtonId"],+ mailSearchToolbarConfig[@"filterMenu"]);+ filterButton.translatesAutoresizingMaskIntoConstraints = NO;+ [toolbarHost addSubview:filterButton];+ [NSLayoutConstraint activateConstraints:@[+ [filterButton.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor],+ [filterButton.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor],+ [filterButton.widthAnchor constraintEqualToConstant:buttonSize],+ [filterButton.heightAnchor constraintEqualToConstant:buttonSize],+ ]];+ }++ if (hasComposeButton) {+ UIButton *composeButton = makeGlassButton(+ mailSearchToolbarConfig[@"composeSystemImageName"] ?: @"square.and.pencil",+ mailSearchToolbarConfig[@"composeButtonId"],+ mailSearchToolbarConfig[@"composeMenu"]);+ composeButton.translatesAutoresizingMaskIntoConstraints = NO;+ [toolbarHost addSubview:composeButton];+ [NSLayoutConstraint activateConstraints:@[+ [composeButton.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor],+ [composeButton.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor],+ [composeButton.widthAnchor constraintEqualToConstant:buttonSize],+ [composeButton.heightAnchor constraintEqualToConstant:buttonSize],+ ]];+ }+ }+#endif+ }++ NSArray<NSDictionary<NSString *, id> *> *navigationToolbarConfigs = config.headerToolbarItems;+ if (mailSearchToolbarConfig != nil) {+ navigationToolbarConfigs = @[];+ }++ NSArray<UIBarButtonItem *> *toolbarItems = [config barButtonItemsFromConfigs:navigationToolbarConfigs+ withCurrentItems:@[]+ navigationItem:navitem];+ if (toolbarItems.count > 0) {+ vc.toolbarItems = toolbarItems;+ [navctr setToolbarHidden:NO animated:animated];+ } else {+ vc.toolbarItems = nil;+ [navctr setToolbarHidden:YES animated:animated];+ } // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items // (setting nav bar visibility should be done after `navitem.*BarButtonItems`).@@ -773,6 +1079,7 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * - (NSArray<UIBarButtonItem *> *)barButtonItemsFromConfigs:(NSArray<NSDictionary<NSString *, id> *> *)dicts withCurrentItems:(NSArray<UIBarButtonItem *> *)currentItems+ navigationItem:(UINavigationItem *)navitem { if (dicts.count == 0) { return currentItems;@@ -781,7 +1088,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * [items addObjectsFromArray:currentItems]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i];- if (dict[@"buttonId"] || dict[@"menu"]) {+ if (dict[@"mailSearchToolbar"]) {+#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)+ if (@available(iOS 26.0, *)) {+ NSNumber *width = dict[@"width"];+ CGFloat resolvedWidth = width != nil ? width.doubleValue : MAX(320.0, [UIScreen mainScreen].bounds.size.width - 56.0);+ CGFloat resolvedHeight = 58.0;+ UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, resolvedWidth, resolvedHeight)];+ container.translatesAutoresizingMaskIntoConstraints = NO;+ [container.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES;+ [container.heightAnchor constraintEqualToConstant:resolvedHeight].active = YES;++ UIStackView *stackView = [[UIStackView alloc] initWithFrame:container.bounds];+ stackView.axis = UILayoutConstraintAxisHorizontal;+ stackView.alignment = UIStackViewAlignmentCenter;+ stackView.distribution = UIStackViewDistributionFill;+ stackView.spacing = 10.0;+ stackView.translatesAutoresizingMaskIntoConstraints = NO;+ [container addSubview:stackView];+ [NSLayoutConstraint activateConstraints:@[+ [stackView.leadingAnchor constraintEqualToAnchor:container.leadingAnchor],Lines 1–500 of 1542. Load more
diff --git a/ios/RNSBarButtonItem.h b/ios/RNSBarButtonItem.h index ea5325ea8d17b1ddfa790ff8dab48ce83142d4d3..acd2fd7ceb7162ed300abc4d3c9f0c24f4d63898 100644 --- a/ios/RNSBarButtonItem.h +++ b/ios/RNSBarButtonItem.h @@ -13,4 +13,8 @@ typedef void (^RNSBarButtonMenuItemAction)(NSString *menuId); menuAction:(RNSBarButtonMenuItemAction)menuAction imageLoader:(RCTImageLoader *)imageLoader; ++ (UIMenu *)initUIMenuWithDict:(NSDictionary<NSString *, id> *)dict + menuAction:(RNSBarButtonMenuItemAction)menuAction + imageLoader:(RCTImageLoader *)imageLoader; + @end diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm index 0eb1f09dee82edd99e3e614938233db5cdeaebfe..73298f10807520970f625e166d7f5dd1338206f1 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -110,6 +110,58 @@ - (instancetype)initWithConfig:(NSDictionary<NSString *, id> *)dict if (dict[@"accessibilityHint"]) { self.accessibilityHint = dict[@"accessibilityHint"]; } +#if !TARGET_OS_TV && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + NSNumber *glassEffectNum = dict[@"glassEffect"]; + if (@available(iOS 26.0, *)) { + if ([glassEffectNum boolValue]) { + UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration]; + configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule; + UIBackgroundConfiguration *background = configuration.background; + background.strokeWidth = 0.0; + background.strokeColor = UIColor.clearColor; + configuration.background = background; + configuration.contentInsets = NSDirectionalEdgeInsetsZero; + if (title != nil) { + configuration.title = title; + } + + UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil]; + button.translatesAutoresizingMaskIntoConstraints = NO; + button.enabled = self.enabled; + button.accessibilityLabel = self.accessibilityLabel; + button.accessibilityHint = self.accessibilityHint; + + CGFloat resolvedWidth = width != nil ? width.doubleValue : 56.0; + [button.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES; + [button.heightAnchor constraintEqualToConstant:50.0].active = YES; + + [[self class] resolveImageFromConfig:dict + imageLoader:imageLoader + completionBlock:^(UIImage *img) { + UIButtonConfiguration *updatedConfiguration = button.configuration; + updatedConfiguration.image = img; + button.configuration = updatedConfiguration; + }]; + + NSDictionary *menu = dict[@"menu"]; + if (menu) { + button.menu = [[self class] initUIMenuWithDict:menu menuAction:menuAction imageLoader:imageLoader]; + button.showsMenuAsPrimaryAction = YES; + } else { + NSString *buttonId = dict[@"buttonId"]; + if (buttonId && action) { + UIAction *pressAction = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull sender) { + action(buttonId); + }]; + [button addAction:pressAction forControlEvents:UIControlEventTouchUpInside]; + } + } + + self.customView = button; + return self; + } + } +#endif #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 if (@available(tvOS 17.0, *)) { diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h index 919b984edc9f91ee9ac26faf257d8a721e26457c..5bb0cd6736ed6bc51db57e2a9326f758f22c51d8 100644 --- a/ios/RNSScreenStackHeaderConfig.h +++ b/ios/RNSScreenStackHeaderConfig.h @@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, retain) NSString *title; +@property (nonatomic, retain) NSString *subtitle; +@property (nonatomic, retain) NSString *largeSubtitle; @property (nonatomic, retain) NSString *titleFontFamily; @property (nonatomic, retain) NSNumber *titleFontSize; @property (nonatomic, retain) NSString *titleFontWeight; @@ -45,9 +47,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL backButtonInCustomView; @property (nonatomic) UISemanticContentAttribute direction; @property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode; +@property (nonatomic) NSInteger navigationItemStyle; @property (nonatomic) RNSBlurEffectStyle blurEffect; @property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerRightBarButtonItems; @property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerLeftBarButtonItems; +@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerCenterBarButtonItems; +@property (nonatomic, copy, nullable) NSArray<NSDictionary<NSString *, id> *> *headerToolbarItems; @property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled; NS_ASSUME_NONNULL_END diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm index 5970e3e3a624b9498b8dedfc16831df03a274d0c..073a72a68688369f39e54b61d1381b8d652cbb83 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -30,6 +30,20 @@ static const NSNumber *const DEFAULT_TITLE_FONT_SIZE = @17; static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; +static NSInteger navigationItemStyleFromCppEquivalent( + react::RNSScreenStackHeaderConfigNavigationItemStyle navigationItemStyle) +{ + switch (navigationItemStyle) { + case react::RNSScreenStackHeaderConfigNavigationItemStyle::Browser: + return UINavigationItemStyleBrowser; + case react::RNSScreenStackHeaderConfigNavigationItemStyle::Editor: + return UINavigationItemStyleEditor; + case react::RNSScreenStackHeaderConfigNavigationItemStyle::Navigator: + default: + return UINavigationItemStyleNavigator; + } +} + @interface RCTImageLoader (Private) - (id<RCTImageCache>)imageCache; @end @@ -47,6 +61,9 @@ + (BOOL)rnscreens_isBlankOrNull:(NSString *)string @end @interface RNSScreenStackHeaderConfig () <RCTMountingTransactionObserving> +#if !TARGET_OS_TV +- (NSArray<UIBarButtonItemGroup *> *)barButtonItemGroupsFromItems:(NSArray<UIBarButtonItem *> *)items; +#endif @end @implementation RNSScreenStackHeaderConfig { @@ -81,6 +98,7 @@ - (void)initProps self.hidden = YES; _reactSubviews = [NSMutableArray new]; _backTitleVisible = YES; + _navigationItemStyle = UINavigationItemStyleNavigator; _blurEffect = RNSBlurEffectStyleNone; } @@ -496,6 +514,10 @@ + (void)updateViewController:(UIViewController *)vc if (shouldHide) { navitem.title = config.title; + if (@available(iOS 26.0, *)) { + navitem.subtitle = config.subtitle; + navitem.largeSubtitle = config.largeSubtitle; + } // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items. [navctr setNavigationBarHidden:YES animated:animated]; @@ -512,11 +534,19 @@ + (void)updateViewController:(UIViewController *)vc } navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever; + + if (@available(iOS 16.0, *)) { + navitem.style = (UINavigationItemStyle)config.navigationItemStyle; + } #endif UINavigationBarAppearance *appearance = [self buildAppearance:vc withConfig:config]; navitem.standardAppearance = appearance; navitem.compactAppearance = appearance; + if (@available(iOS 26.0, *)) { + navitem.subtitle = config.subtitle; + navitem.largeSubtitle = config.largeSubtitle; + } // appearance does not apply to the tvOS so we need to use lagacy customization #if TARGET_OS_TV @@ -637,10 +667,286 @@ + (void)updateViewController:(UIViewController *)vc // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) navitem.title = config.title; - navitem.leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems - withCurrentItems:navitem.leftBarButtonItems]; - navitem.rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems - withCurrentItems:navitem.rightBarButtonItems]; + NSArray<UIBarButtonItem *> *leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems + withCurrentItems:navitem.leftBarButtonItems + navigationItem:navitem]; + NSArray<UIBarButtonItem *> *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems + withCurrentItems:navitem.rightBarButtonItems + navigationItem:navitem]; + NSArray<UIBarButtonItem *> *centerBarButtonItems = [config barButtonItemsFromConfigs:config.headerCenterBarButtonItems + withCurrentItems:@[] + navigationItem:navitem]; +#if !TARGET_OS_TV + if (@available(iOS 16.0, *)) { + navitem.leadingItemGroups = [config barButtonItemGroupsFromItems:leftBarButtonItems]; + navitem.trailingItemGroups = [config barButtonItemGroupsFromItems:rightBarButtonItems]; + if (@available(iOS 26.0, *)) { + navitem.centerItemGroups = [config barButtonItemGroupsFromItems:centerBarButtonItems]; + } + navitem.leftBarButtonItems = nil; + navitem.rightBarButtonItems = nil; + } else { + navitem.leftBarButtonItems = leftBarButtonItems; + navitem.rightBarButtonItems = rightBarButtonItems; + } +#else + navitem.leftBarButtonItems = leftBarButtonItems; + navitem.rightBarButtonItems = rightBarButtonItems; +#endif + NSDictionary<NSString *, id> *mailSearchToolbarConfig = nil; + for (NSDictionary<NSString *, id> *toolbarConfig in config.headerToolbarItems) { + if (toolbarConfig[@"mailSearchToolbar"]) { + mailSearchToolbarConfig = toolbarConfig; + break; + } + } + if (mailSearchToolbarConfig == nil) { + for (NSDictionary<NSString *, id> *rightConfig in config.headerRightBarButtonItems) { + if ([rightConfig[@"bottomMailSearchToolbar"] boolValue]) { + mailSearchToolbarConfig = rightConfig; + break; + } + } + } + + static NSInteger const RNSMailSearchToolbarViewTag = 260628; + UIView *chromeHostView = vc.view; + UIView *existingMailSearchToolbar = [chromeHostView viewWithTag:RNSMailSearchToolbarViewTag]; + if (existingMailSearchToolbar == nil) { + existingMailSearchToolbar = [vc.view viewWithTag:RNSMailSearchToolbarViewTag]; + } + [existingMailSearchToolbar removeFromSuperview]; + + if (mailSearchToolbarConfig != nil) { +#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + if (@available(iOS 26.0, *)) { + CGFloat horizontalInset = 18.0; + NSNumber *width = mailSearchToolbarConfig[@"width"]; + CGFloat hostWidth = chromeHostView.bounds.size.width; + if (hostWidth <= 0.0) { + hostWidth = vc.view.bounds.size.width; + } + if (hostWidth <= 0.0) { + hostWidth = [UIScreen mainScreen].bounds.size.width; + } + CGFloat fallbackWidth = MIN(560.0, MAX(300.0, hostWidth - (horizontalInset * 2.0))); + CGFloat resolvedWidth = width != nil ? width.doubleValue : fallbackWidth; + if (hostWidth > 0.0) { + resolvedWidth = MIN(resolvedWidth, MAX(260.0, hostWidth - 12.0)); + } + + CGFloat toolbarHeight = 48.0; + CGFloat toolbarRadius = toolbarHeight / 2.0; + CGFloat buttonSize = toolbarHeight; + CGFloat sideButtonReserve = buttonSize + 8.0; + CGFloat searchFieldHeight = 42.0; + CGFloat toolbarBottomSpacing = 4.0; + UIFont *searchFont = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle3]; + NSDictionary<NSAttributedStringKey, id> *placeholderAttributes = @{ + NSForegroundColorAttributeName : UIColor.secondaryLabelColor, + NSFontAttributeName : searchFont, + }; + UIKeyboardLayoutGuide *keyboardLayoutGuide = chromeHostView.keyboardLayoutGuide; + + UIView *toolbarHost = [[UIView alloc] init]; + toolbarHost.tag = RNSMailSearchToolbarViewTag; + toolbarHost.translatesAutoresizingMaskIntoConstraints = NO; + [chromeHostView addSubview:toolbarHost]; + // Keyboard avoidance is best-effort: on the iOS 27 beta the keyboard + // layout guide no longer rests at the bottom safe-area edge while the + // keyboard is hidden, which pushed the toolbar offscreen. The required + // resting position is the safe area; the keyboard guide only pulls the + // toolbar up when it actually tracks a visible keyboard. + NSLayoutConstraint *keyboardAvoidConstraint = + [toolbarHost.bottomAnchor constraintEqualToAnchor:keyboardLayoutGuide.topAnchor + constant:-toolbarBottomSpacing]; + keyboardAvoidConstraint.priority = UILayoutPriorityDefaultHigh; + NSLayoutConstraint *restingBottomConstraint = + [toolbarHost.bottomAnchor constraintEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor + constant:-toolbarBottomSpacing]; + restingBottomConstraint.priority = UILayoutPriorityDefaultLow; + [NSLayoutConstraint activateConstraints:@[ + [toolbarHost.centerXAnchor constraintEqualToAnchor:chromeHostView.centerXAnchor], + [toolbarHost.bottomAnchor constraintLessThanOrEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor + constant:-toolbarBottomSpacing], + keyboardAvoidConstraint, + restingBottomConstraint, + [toolbarHost.widthAnchor constraintEqualToConstant:resolvedWidth], + [toolbarHost.heightAnchor constraintEqualToConstant:toolbarHeight], + ]]; + [chromeHostView bringSubviewToFront:toolbarHost]; + + UIGlassEffect *glassEffect = [UIGlassEffect effectWithStyle:UIGlassEffectStyleRegular]; + glassEffect.interactive = YES; + UIVisualEffectView *glassView = [[UIVisualEffectView alloc] initWithEffect:glassEffect]; + glassView.clipsToBounds = YES; + glassView.layer.cornerRadius = toolbarRadius; + glassView.translatesAutoresizingMaskIntoConstraints = NO; + [toolbarHost addSubview:glassView]; + BOOL hasFilterButton = + mailSearchToolbarConfig[@"filterButtonId"] != nil || mailSearchToolbarConfig[@"filterMenu"] != nil; + BOOL hasComposeButton = + mailSearchToolbarConfig[@"composeButtonId"] != nil || mailSearchToolbarConfig[@"composeMenu"] != nil; + CGFloat glassLeadingInset = hasFilterButton ? sideButtonReserve : 0.0; + CGFloat glassTrailingInset = hasComposeButton ? -sideButtonReserve : 0.0; + [NSLayoutConstraint activateConstraints:@[ + [glassView.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor constant:glassLeadingInset], + [glassView.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor constant:glassTrailingInset], + [glassView.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], + [glassView.heightAnchor constraintEqualToConstant:toolbarHeight], + ]]; + + void (^emitButtonPress)(NSString *) = ^(NSString *buttonId) { + auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>( + config->_eventEmitter); + if (eventEmitter && buttonId) { + eventEmitter->onPressHeaderBarButtonItem( + facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonItem{ + .buttonId = std::string([buttonId UTF8String])}); + } + }; + + void (^emitMenuPress)(NSString *) = ^(NSString *menuId) { + auto eventEmitter = std::static_pointer_cast<const facebook::react::RNSScreenStackHeaderConfigEventEmitter>( + config->_eventEmitter); + if (eventEmitter && menuId) { + eventEmitter->onPressHeaderBarButtonMenuItem( + facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonMenuItem{ + .menuId = std::string([menuId UTF8String])}); + } + }; + + UIButton *(^makeGlassButton)(NSString *, NSString *, NSDictionary *) = + ^UIButton *(NSString *systemImageName, NSString *buttonId, NSDictionary *menuConfig) { + UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration]; + configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule; + configuration.image = [UIImage systemImageNamed:systemImageName ?: @"circle"]; + configuration.baseForegroundColor = UIColor.labelColor; + UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil]; + if (menuConfig != nil) { + button.menu = [RNSBarButtonItem initUIMenuWithDict:menuConfig + menuAction:emitMenuPress + imageLoader:config->_imageLoader]; + button.showsMenuAsPrimaryAction = YES; + } else if (buttonId != nil) { + UIAction *action = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { + emitButtonPress(buttonId); + }]; + [button addAction:action forControlEvents:UIControlEventTouchUpInside]; + } + return button; + }; + + BOOL useFallbackSearchField = [mailSearchToolbarConfig[@"useFallbackSearchField"] boolValue]; + UISearchBar *searchBar = + !useFallbackSearchField && navitem.searchController != nil ? navitem.searchController.searchBar : nil; + NSString *placeholder = mailSearchToolbarConfig[@"placeholder"]; + if (searchBar != nil) { + if (placeholder != nil) { + searchBar.placeholder = placeholder; + } + searchBar.hidden = NO; + searchBar.alpha = 1.0; + searchBar.userInteractionEnabled = YES; + searchBar.searchBarStyle = UISearchBarStyleMinimal; + searchBar.backgroundImage = [UIImage new]; + searchBar.searchTextField.hidden = NO; + searchBar.searchTextField.alpha = 1.0; + searchBar.searchTextField.backgroundColor = UIColor.clearColor; + searchBar.searchTextField.font = searchFont; + searchBar.searchTextField.adjustsFontForContentSizeCategory = YES; + searchBar.searchTextField.textColor = UIColor.labelColor; + searchBar.searchTextField.tintColor = UIColor.labelColor; + if (placeholder != nil) { + searchBar.searchTextField.attributedPlaceholder = + [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes]; + } + searchBar.translatesAutoresizingMaskIntoConstraints = NO; + [glassView.contentView addSubview:searchBar]; + [NSLayoutConstraint activateConstraints:@[ + [searchBar.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:8.0], + [searchBar.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-8.0], + [searchBar.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor], + [searchBar.heightAnchor constraintEqualToConstant:searchFieldHeight], + ]]; + } else { + UISearchTextField *searchField = [UISearchTextField new]; + if (placeholder != nil) { + searchField.placeholder = placeholder; + searchField.attributedPlaceholder = + [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes]; + } + NSString *searchTextChangeId = mailSearchToolbarConfig[@"searchTextChangeId"]; + if (searchTextChangeId != nil) { + [searchField addAction:[UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { + UISearchTextField *field = (UISearchTextField *)action.sender; + emitButtonPress([NSString stringWithFormat:@"%@:%@", searchTextChangeId, field.text ?: @""]); + }] + forControlEvents:UIControlEventEditingChanged]; + } + searchField.borderStyle = UITextBorderStyleNone; + searchField.font = searchFont; + searchField.adjustsFontForContentSizeCategory = YES; + searchField.textColor = UIColor.labelColor; + searchField.tintColor = UIColor.labelColor; + searchField.translatesAutoresizingMaskIntoConstraints = NO; + [glassView.contentView addSubview:searchField]; + [NSLayoutConstraint activateConstraints:@[ + [searchField.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:14.0], + [searchField.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-14.0], + [searchField.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor], + [searchField.heightAnchor constraintEqualToConstant:searchFieldHeight], + ]]; + } + + if (hasFilterButton) { + UIButton *filterButton = makeGlassButton( + mailSearchToolbarConfig[@"filterSystemImageName"] ?: @"line.3.horizontal.decrease", + mailSearchToolbarConfig[@"filterButtonId"], + mailSearchToolbarConfig[@"filterMenu"]); + filterButton.translatesAutoresizingMaskIntoConstraints = NO; + [toolbarHost addSubview:filterButton]; + [NSLayoutConstraint activateConstraints:@[ + [filterButton.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor], + [filterButton.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], + [filterButton.widthAnchor constraintEqualToConstant:buttonSize], + [filterButton.heightAnchor constraintEqualToConstant:buttonSize], + ]]; + } + + if (hasComposeButton) { + UIButton *composeButton = makeGlassButton( + mailSearchToolbarConfig[@"composeSystemImageName"] ?: @"square.and.pencil", + mailSearchToolbarConfig[@"composeButtonId"], + mailSearchToolbarConfig[@"composeMenu"]); + composeButton.translatesAutoresizingMaskIntoConstraints = NO; + [toolbarHost addSubview:composeButton]; + [NSLayoutConstraint activateConstraints:@[ + [composeButton.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor], + [composeButton.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], + [composeButton.widthAnchor constraintEqualToConstant:buttonSize], + [composeButton.heightAnchor constraintEqualToConstant:buttonSize], + ]]; + } + } +#endif + } + + NSArray<NSDictionary<NSString *, id> *> *navigationToolbarConfigs = config.headerToolbarItems; + if (mailSearchToolbarConfig != nil) { + navigationToolbarConfigs = @[]; + } + + NSArray<UIBarButtonItem *> *toolbarItems = [config barButtonItemsFromConfigs:navigationToolbarConfigs + withCurrentItems:@[] + navigationItem:navitem]; + if (toolbarItems.count > 0) { + vc.toolbarItems = toolbarItems; + [navctr setToolbarHidden:NO animated:animated]; + } else { + vc.toolbarItems = nil; + [navctr setToolbarHidden:YES animated:animated]; + } // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items // (setting nav bar visibility should be done after `navitem.*BarButtonItems`). @@ -773,6 +1079,7 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * - (NSArray<UIBarButtonItem *> *)barButtonItemsFromConfigs:(NSArray<NSDictionary<NSString *, id> *> *)dicts withCurrentItems:(NSArray<UIBarButtonItem *> *)currentItems + navigationItem:(UINavigationItem *)navitem { if (dicts.count == 0) { return currentItems; @@ -781,7 +1088,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * [items addObjectsFromArray:currentItems]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; - if (dict[@"buttonId"] || dict[@"menu"]) { + if (dict[@"mailSearchToolbar"]) { +#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) + if (@available(iOS 26.0, *)) { + NSNumber *width = dict[@"width"]; + CGFloat resolvedWidth = width != nil ? width.doubleValue : MAX(320.0, [UIScreen mainScreen].bounds.size.width - 56.0); + CGFloat resolvedHeight = 58.0; + UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, resolvedWidth, resolvedHeight)]; + container.translatesAutoresizingMaskIntoConstraints = NO; + [container.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES; + [container.heightAnchor constraintEqualToConstant:resolvedHeight].active = YES; + + UIStackView *stackView = [[UIStackView alloc] initWithFrame:container.bounds]; + stackView.axis = UILayoutConstraintAxisHorizontal; + stackView.alignment = UIStackViewAlignmentCenter; + stackView.distribution = UIStackViewDistributionFill; + stackView.spacing = 10.0; + stackView.translatesAutoresizingMaskIntoConstraints = NO; + [container addSubview:stackView]; + [NSLayoutConstraint activateConstraints:@[ + [stackView.leadingAnchor constraintEqualToAnchor:container.leadingAnchor],