#import "RNSBarButtonItem.h" #import #import #import "RNSDefines.h" #import "RNSImageLoadingHelper.h" static UIMenuOptions RNSMakeUIMenuOptionsFromConfig(NSDictionary *config); @implementation RNSBarButtonItem { NSString *_buttonId; RNSBarButtonItemAction _itemAction; } - (instancetype)initWithConfig:(NSDictionary *)dict action:(RNSBarButtonItemAction)action menuAction:(RNSBarButtonMenuItemAction)menuAction imageLoader:(RCTImageLoader *)imageLoader { self = [super init]; if (!self) { return self; } NSString *title = dict[@"title"]; NSDictionary *imageSourceObj = dict[@"imageSource"]; NSDictionary *templateSourceObj = dict[@"templateSource"]; NSString *sfSymbolName = dict[@"sfSymbolName"]; NSString *xcassetName = dict[@"xcassetName"]; if (imageSourceObj != nil || templateSourceObj != nil) { BOOL isTemplate = imageSourceObj != nil ? NO : YES; NSDictionary *source = imageSourceObj != nil ? imageSourceObj : templateSourceObj; [RNSImageLoadingHelper loadImageSyncIfPossibleFromJsonSource:source withImageLoader:imageLoader asTemplate:isTemplate completionBlock:^(UIImage *image) { self.image = image; }]; } else if (sfSymbolName != nil) { self.image = [UIImage systemImageNamed:sfSymbolName]; } else if (xcassetName != nil) { self.image = [UIImage imageNamed:xcassetName]; } if (title != nil) { self.title = title; NSDictionary *titleStyle = dict[@"titleStyle"]; if (titleStyle != nil) { [self setTitleStyleFromConfig:titleStyle]; } } id tintColorObj = dict[@"tintColor"]; if (tintColorObj) { self.tintColor = [RCTConvert UIColor:tintColorObj]; } #if !TARGET_OS_TV NSNumber *selectedNum = dict[@"selected"]; if (selectedNum != nil) { self.selected = [selectedNum boolValue]; } #endif NSNumber *disabledNum = dict[@"disabled"]; if (disabledNum != nil) { self.enabled = ![disabledNum boolValue]; } NSNumber *width = dict[@"width"]; if (width) { self.width = [width doubleValue]; } #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 if (@available(tvOS 17.0, *)) { NSNumber *changesSelectionAsPrimaryActionNum = dict[@"changesSelectionAsPrimaryAction"]; if (changesSelectionAsPrimaryActionNum != nil) { self.changesSelectionAsPrimaryAction = [changesSelectionAsPrimaryActionNum boolValue]; } } #endif #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) if (@available(iOS 26.0, *)) { NSNumber *hidesSharedBackgroundNum = dict[@"hidesSharedBackground"]; if (hidesSharedBackgroundNum != nil) { self.hidesSharedBackground = [hidesSharedBackgroundNum boolValue]; } NSNumber *sharesBackgroundNum = dict[@"sharesBackground"]; if (sharesBackgroundNum != nil) { self.sharesBackground = [sharesBackgroundNum boolValue]; } NSString *identifier = dict[@"identifier"]; if (identifier != nil) { self.identifier = identifier; } NSDictionary *badgeConfig = dict[@"badge"]; if (badgeConfig != nil) { [self setBadgeFromConfig:badgeConfig]; } } #endif NSString *variant = dict[@"variant"]; if (variant) { if ([variant isEqualToString:@"done"]) { self.style = UIBarButtonItemStyleDone; } else if ([variant isEqualToString:@"prominent"]) { #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) if (@available(iOS 26.0, *)) { self.style = UIBarButtonItemStyleProminent; } #endif } else { self.style = UIBarButtonItemStylePlain; } } if (dict[@"accessibilityLabel"]) { self.accessibilityLabel = dict[@"accessibilityLabel"]; } if (dict[@"accessibilityHint"]) { self.accessibilityHint = dict[@"accessibilityHint"]; } #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 if (@available(tvOS 17.0, *)) { NSDictionary *menu = dict[@"menu"]; if (menu) { self.menu = [[self class] initUIMenuWithDict:menu menuAction:menuAction]; } } #endif NSString *buttonId = dict[@"buttonId"]; if (buttonId && action) { self.target = self; self.action = @selector(handleBarButtonItemPress:); _itemAction = action; _buttonId = buttonId; } return self; } + (UIMenu *)initUIMenuWithDict:(NSDictionary *)dict menuAction:(RNSBarButtonMenuItemAction)menuAction { NSArray *items = dict[@"items"]; NSMutableArray *elements = [NSMutableArray new]; if (items.count > 0) { for (NSDictionary *item in items) { NSString *menuId = item[@"menuId"]; if (menuId) { UIAction *actionItem = [self createActionItemFromConfig:item menuAction:menuAction]; [elements addObject:actionItem]; } else { UIMenu *childMenu = [self initUIMenuWithDict:item menuAction:menuAction]; if (childMenu) { [elements addObject:childMenu]; } } } } NSString *title = dict[@"title"]; NSString *sfSymbolName = dict[@"sfSymbolName"]; NSString *xcassetName = dict[@"xcassetName"]; UIImage* image = nil; if (sfSymbolName != nil) { image = [UIImage systemImageNamed:sfSymbolName]; } else if (xcassetName != nil) { image = [UIImage imageNamed:xcassetName]; } return [UIMenu menuWithTitle:title image:image identifier:nil options:RNSMakeUIMenuOptionsFromConfig(dict) children:elements]; } + (UIAction *)createActionItemFromConfig:(NSDictionary *)dict menuAction:(RNSBarButtonMenuItemAction)menuAction { NSString *menuId = dict[@"menuId"]; NSString *title = dict[@"title"]; NSString *sfSymbolName = dict[@"sfSymbolName"]; NSString *xcassetName = dict[@"xcassetName"]; UIImage *image = nil; if (sfSymbolName != nil) { image = [UIImage systemImageNamed:sfSymbolName]; } else if (xcassetName != nil) { image = [UIImage imageNamed:xcassetName]; } UIAction *actionElement = [UIAction actionWithTitle:title image:image identifier:nil handler:^(__kindof UIAction *_Nonnull a) { menuAction(menuId); }]; NSString *discoverabilityLabel = dict[@"discoverabilityLabel"]; if (discoverabilityLabel != nil) { actionElement.discoverabilityTitle = discoverabilityLabel; } NSString *state = dict[@"state"]; if ([state isEqualToString:@"on"]) { actionElement.state = UIMenuElementStateOn; } else if ([state isEqualToString:@"off"]) { actionElement.state = UIMenuElementStateOff; } else if ([state isEqualToString:@"mixed"]) { actionElement.state = UIMenuElementStateMixed; } NSNumber *disabled = dict[@"disabled"]; NSNumber *hidden = dict[@"hidden"]; NSNumber *destructive = dict[@"destructive"]; NSNumber *keepsMenuPresented = dict[@"keepsMenuPresented"]; if (disabled != nil && [disabled boolValue]) { actionElement.attributes |= UIMenuElementAttributesDisabled; } if (hidden != nil && [hidden boolValue]) { actionElement.attributes |= UIMenuElementAttributesHidden; } if (destructive != nil && [destructive boolValue]) { actionElement.attributes |= UIMenuElementAttributesDestructive; } if (keepsMenuPresented != nil && [keepsMenuPresented boolValue]) { #if RNS_IPHONE_OS_VERSION_AVAILABLE(16_0) if (@available(iOS 16.0, *)) { actionElement.attributes |= UIMenuElementAttributesKeepsMenuPresented; } #endif #if TARGET_OS_TV && __TV_OS_VERSION_MAX_ALLOWED >= 160000 if (@available(tvOS 16.0, *)) { actionElement.attributes |= UIMenuElementAttributesKeepsMenuPresented; } #endif } NSString *subtitle = dict[@"subtitle"]; if (subtitle != nil) { actionElement.subtitle = subtitle; } return actionElement; } - (void)handleBarButtonItemPress:(UIBarButtonItem *)item { if (_itemAction && _buttonId) { _itemAction(_buttonId); } } - (void)setTitleStyleFromConfig:(NSDictionary *)titleStyle { NSString *fontFamily = titleStyle[@"fontFamily"]; NSNumber *fontSize = titleStyle[@"fontSize"]; NSString *fontWeight = titleStyle[@"fontWeight"]; NSMutableDictionary *attrs = [NSMutableDictionary new]; if (fontFamily || fontWeight) { NSNumber *resolvedFontSize = fontSize; if (!resolvedFontSize) { #if TARGET_OS_TV resolvedFontSize = [NSNumber numberWithDouble:17.0]; #else resolvedFontSize = [NSNumber numberWithFloat:[UIFont labelFontSize]]; #endif } attrs[NSFontAttributeName] = [RCTFont updateFont:nil withFamily:fontFamily size:resolvedFontSize weight:fontWeight style:nil variant:nil scaleMultiplier:1.0]; } else { CGFloat resolvedFontSize = fontSize ? [fontSize floatValue] : 0; if (resolvedFontSize == 0) { #if TARGET_OS_TV resolvedFontSize = 17.0; #else resolvedFontSize = [UIFont labelFontSize]; #endif } attrs[NSFontAttributeName] = [UIFont systemFontOfSize:resolvedFontSize]; } id titleColor = titleStyle[@"color"]; if (titleColor) { attrs[NSForegroundColorAttributeName] = [RCTConvert UIColor:titleColor]; } [self setTitleTextAttributes:attrs forState:UIControlStateNormal]; [self setTitleTextAttributes:attrs forState:UIControlStateHighlighted]; [self setTitleTextAttributes:attrs forState:UIControlStateDisabled]; [self setTitleTextAttributes:attrs forState:UIControlStateSelected]; [self setTitleTextAttributes:attrs forState:UIControlStateFocused]; } #if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) - (void)setBadgeFromConfig:(NSDictionary *)badgeObj { if (@available(iOS 26.0, *)) { UIBarButtonItemBadge *badge = [UIBarButtonItemBadge badgeWithString:badgeObj[@"value"]]; NSDictionary *style = badgeObj[@"style"]; if (style) { id colorObj = style[@"color"]; if (colorObj) { badge.foregroundColor = [RCTConvert UIColor:colorObj]; } id backgroundColorObj = style[@"backgroundColor"]; if (backgroundColorObj) { badge.backgroundColor = [RCTConvert UIColor:backgroundColorObj]; } NSString *fontFamily = style[@"fontFamily"]; NSNumber *fontSize = style[@"fontSize"]; NSString *fontWeight = style[@"fontWeight"]; if (fontSize || fontWeight) { badge.font = [RCTFont updateFont:nil withFamily:fontFamily size:fontSize weight:fontWeight style:nil variant:nil scaleMultiplier:1.0]; } else { CGFloat resolvedFontSize = fontSize ? [fontSize floatValue] : 0; if (resolvedFontSize == 0) { #if TARGET_OS_TV resolvedFontSize = 17.0; #else resolvedFontSize = [UIFont labelFontSize]; #endif } badge.font = [UIFont systemFontOfSize:resolvedFontSize]; } } self.badge = badge; } } #endif @end UIMenuOptions RNSMakeUIMenuOptionsFromConfig(NSDictionary *config) { UIMenuOptions options = 0; NSNumber *singleSelection = config[@"singleSelection"]; NSNumber *displayAsPalette = config[@"displayAsPalette"]; NSNumber *displayInline = config[@"displayInline"]; NSNumber *destructive = config[@"destructive"]; if (singleSelection != nil && [singleSelection boolValue]) { options |= UIMenuOptionsSingleSelection; } #if RNS_IPHONE_OS_VERSION_AVAILABLE(17_0) if (@available(iOS 17.0, *)) { if (displayAsPalette != nil && [displayAsPalette boolValue]) { options |= UIMenuOptionsDisplayAsPalette; } } #endif // RNS_IPHONE_OS_VERSION_AVAILABLE(17_0) if (displayInline != nil && [displayInline boolValue]) { options |= UIMenuOptionsDisplayInline; } if (destructive != nil && [destructive boolValue]) { options |= UIMenuOptionsDestructive; } return options; }