CocoaPods trunk is moving to be read-only. Read more on the blog, there are 16 months to go.

WGBEasyMenu 1.0.1

WGBEasyMenu 1.0.1

Maintained by GBCoderWang.



  • By
  • CoderWGB

EasyMenu

Platform Language License Version

EasyMenu 是一个基于 UIMenuUIContextMenuInteraction 的轻量级封装,为任何 UIView 提供极其简单、易于集成的上下文菜单解决方案,支持预览功能和丰富的菜单样式控制。

✨ 新功能亮点 (v1.0.1)

🔍 预览功能 (Preview Support)

  • 长按预览:显示自定义预览视图控制器
  • 用力按压跳转:3D Touch/Haptic Touch 深度按压直接跳转到详情页面
  • 无缝体验:预览到跳转的平滑过渡动画

🎨 菜单样式控制 (Menu Styling)

  • 菜单项状态:支持选中/未选中/混合状态显示
  • 样式属性:破坏性操作(红色)、禁用状态(灰色不可点击)
  • 视觉反馈:与系统原生菜单样式完全一致

简介

在 iOS 13 之后,苹果引入了 UIMenu,它提供了一种现代化的方式来展示上下文菜单。然而,直接使用 UIContextMenuInteraction 和其代理方法相对繁琐,尤其是在需要为多个视图或动态内容(如 UITableViewCell)提供菜单时。

EasyMenu 的诞生就是为了解决这个问题。它通过 UIView 的 Category 扩展,提供简洁的 API,让你只需几行代码就能为任何视图添加功能强大的静态或动态菜单,同时自动处理了所有底层的代理和生命周期管理。

特性

  • 简洁 API: 使用配置类 EasyMenuConfigEasyMenuItem,以声明式的方式构建菜单
  • 🆕 预览功能: 支持长按预览和用力按压跳转,提供类似系统照片应用的体验
  • 🆕 样式控制: 支持菜单项状态(选中/禁用)和样式(破坏性操作)控制
  • 静态与动态: 支持一次性配置的静态菜单和根据上下文动态生成的菜单
  • 自动管理: 无需手动处理 UIContextMenuInteraction 的创建、添加和代理设置
  • 轻量级: 无任何第三方依赖,仅包含几个核心文件,易于集成
  • 子菜单支持: 轻松创建包含层级关系的子菜单
  • 系统兼容性: 内置 iOS 版本检查,安全处理系统图标兼容性
  • 便捷扩展: 针对 UIButton 和 UITableView 提供专门的便捷方法

要求

  • iOS 13.0+
  • Xcode 11.0+

安装

CocoaPods

pod 'EasyMenu'

手动安装

EasyMenu 文件夹下的所有 .h.m 文件拖入你的 Xcode 项目中:

  • EasyMenuItem.h/m
  • EasyMenuConfig.h/m
  • EasyMenuManager.h/m
  • EasyMenuCompatibility.h/m
  • UIView+EasyMenu.h/m
  • UIButton+EasyMenu.h/m
  • UITableView+EasyMenu.h/m

在需要使用的地方,导入主头文件:

#import "EasyMenu.h"

核心 API

基础方法

// 为视图添加静态菜单
- (void)em_addMenuWithConfig:(EasyMenuConfig *)config;

// 为视图添加动态菜单
- (void)em_addMenuWithDynamicProvider:(DynamicMenuProvider)provider;

// 移除菜单
- (void)em_removeMenu;

🆕 预览功能 API

// 创建带预览功能的菜单项
+ (instancetype)itemWithTitle:(NSString *)title
                        image:(nullable UIImage *)image
                      handler:(nullable void (^)(void))handler
              previewProvider:(nullable UIViewController * (^)(void))previewProvider;

// 创建带预览和提交处理的菜单项
+ (instancetype)itemWithTitle:(NSString *)title
                        image:(nullable UIImage *)image
                      handler:(nullable void (^)(void))handler
              previewProvider:(nullable UIViewController * (^)(void))previewProvider
                commitHandler:(nullable void (^)(UIViewController *))commitHandler;

🆕 样式控制 API

// 创建带样式的菜单项
+ (instancetype)itemWithTitle:(NSString *)title
                        image:(nullable UIImage *)image
                      handler:(nullable void (^)(void))handler
                        state:(UIMenuElementState)state
                   attributes:(UIMenuElementAttributes)attributes;

UIButton 扩展

// 添加常用菜单(复制、分享、自定义操作)
- (void)em_addCommonMenuWithCopyText:(nullable NSString *)copyText
                         shareObject:(nullable id)shareObject
                       customActions:(nullable NSArray<EasyMenuItem *> *)customActions;

UITableView 扩展

// 为表格启用上下文菜单
- (void)em_enableContextMenuWithEditHandler:(nullable void(^)(NSIndexPath *indexPath))editHandler
                              deleteHandler:(nullable void(^)(NSIndexPath *indexPath))deleteHandler
                               shareHandler:(nullable void(^)(NSIndexPath *indexPath, id data))shareHandler;

使用示例

🆕 示例 1: 带预览功能的菜单

#import "EasyMenu.h"

- (void)setupPreviewMenu {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@"长按预览" forState:UIControlStateNormal];
    [self.view addSubview:button];
    
    EasyMenuConfig *config = [EasyMenuConfig configWithTitle:@"预览菜单" items:@[
        [EasyMenuItem itemWithTitle:@"查看详情"
                              image:[EasyMenuCompatibility safeSystemImageNamed:@"eye"]
                            handler:^{
            NSLog(@"查看详情 - 直接操作");
        } previewProvider:^UIViewController *{
            // 返回预览视图控制器
            PreviewViewController *previewVC = [[PreviewViewController alloc] 
                                               initWithTitle:@"预览内容" 
                                                     content:@"这是预览视图\n长按查看预览\n用力按压跳转详情"];
            return previewVC;
        } commitHandler:^(UIViewController *previewViewController) {
            // 处理用力按压后的跳转
            UINavigationController *navController = [[UINavigationController alloc] 
                                                    initWithRootViewController:previewViewController];
            navController.modalPresentationStyle = UIModalPresentationFormSheet;
            
            dispatch_async(dispatch_get_main_queue(), ^{
                UIViewController *topVC = [self topViewController];
                if (topVC && !topVC.presentedViewController) {
                    [topVC presentViewController:navController animated:YES completion:nil];
                }
            });
        }],
        [EasyMenuItem itemWithTitle:@"编辑" 
                              image:[EasyMenuCompatibility safeSystemImageNamed:@"pencil"] 
                            handler:^{
            NSLog(@"编辑操作");
        }]
    ]];
    
    [button em_addMenuWithConfig:config];
}

🆕 示例 2: 样式化菜单

- (void)setupStyledMenu {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@"样式化菜单" forState:UIControlStateNormal];
    [self.view addSubview:button];
    
    NSMutableArray<EasyMenuItem *> *items = [NSMutableArray array];
    
    // 普通菜单项
    [items addObject:[EasyMenuItem itemWithTitle:@"普通项目" 
                                           image:[EasyMenuCompatibility safeSystemImageNamed:@"circle"] 
                                         handler:^{ NSLog(@"普通项目"); }]];
    
    // 选中状态的菜单项
    if (@available(iOS 13.0, *)) {
        EasyMenuItem *checkedItem = [EasyMenuItem itemWithTitle:@"已选中项目" 
                                                          image:[EasyMenuCompatibility safeSystemImageNamed:@"checkmark.circle.fill"] 
                                                        handler:^{ NSLog(@"已选中项目"); } 
                                                          state:UIMenuElementStateOn 
                                                     attributes:0];
        [items addObject:checkedItem];
        
        // 禁用状态的菜单项
        EasyMenuItem *disabledItem = [EasyMenuItem itemWithTitle:@"禁用项目" 
                                                           image:[EasyMenuCompatibility safeSystemImageNamed:@"xmark.circle"] 
                                                         handler:^{ NSLog(@"这不应该被调用"); } 
                                                           state:UIMenuElementStateOff 
                                                      attributes:UIMenuElementAttributesDisabled];
        [items addObject:disabledItem];
        
        // 破坏性操作菜单项(红色)
        EasyMenuItem *destructiveItem = [EasyMenuItem itemWithTitle:@"删除所有" 
                                                              image:[EasyMenuCompatibility safeSystemImageNamed:@"trash.fill"] 
                                                            handler:^{ NSLog(@"删除所有"); } 
                                                              state:UIMenuElementStateOff 
                                                         attributes:UIMenuElementAttributesDestructive];
        [items addObject:destructiveItem];
    }
    
    EasyMenuConfig *config = [EasyMenuConfig configWithTitle:@"样式菜单" items:items];
    [button em_addMenuWithConfig:config];
}

示例 3: 基础按钮菜单

- (void)setupBasicButton {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@"长按显示菜单" forState:UIControlStateNormal];
    [self.view addSubview:button];
    
    EasyMenuConfig *config = [EasyMenuConfig configWithTitle:@"操作菜单" items:@[
        [EasyMenuItem itemWithTitle:@"复制" 
                              image:[EasyMenuCompatibility safeSystemImageNamed:@"doc.on.doc"] 
                            handler:^{
            NSLog(@"复制操作");
        }],
        [EasyMenuItem itemWithTitle:@"分享" 
                              image:[EasyMenuCompatibility safeSystemImageNamed:@"square.and.arrow.up"] 
                            handler:^{
            NSLog(@"分享操作");
        }],
        [EasyMenuItem itemWithTitle:@"删除" 
                              image:[EasyMenuCompatibility safeSystemImageNamed:@"trash"] 
                            handler:^{
            NSLog(@"删除操作");
        }]
    ]];
    
    [button em_addMenuWithConfig:config];
}

示例 4: 动态菜单

- (void)setupDynamicMenu {
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 100)];
    [self.view addSubview:view];
    
    [view em_addMenuWithDynamicProvider:^EasyMenuConfig * _Nullable(__kindof UIView *targetView) {
        // 根据当前状态动态生成菜单
        NSDate *now = [NSDate date];
        NSString *timeString = [NSDateFormatter localizedStringFromDate:now 
                                                              dateStyle:NSDateFormatterNoStyle 
                                                              timeStyle:NSDateFormatterMediumStyle];
        
        return [EasyMenuConfig configWithTitle:@"动态菜单" items:@[
            [EasyMenuItem itemWithTitle:[NSString stringWithFormat:@"当前时间: %@", timeString] 
                                  image:nil 
                                handler:^{
                NSLog(@"显示时间信息");
            }]
        ]];
    }];
}

示例 5: 嵌套子菜单

- (void)setupNestedMenu {
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button setTitle:@"嵌套菜单" forState:UIControlStateNormal];
    [self.view addSubview:button];
    
    EasyMenuConfig *config = [EasyMenuConfig configWithTitle:@"主菜单" items:@[
        [EasyMenuItem itemWithTitle:@"主要操作" 
                              image:[EasyMenuCompatibility safeSystemImageNamed:@"star"] 
                            handler:^{
            NSLog(@"主要操作");
        }],
        [EasyMenuItem subMenuWithTitle:@"更多选项" 
                                 image:[EasyMenuCompatibility safeSystemImageNamed:@"ellipsis"] 
                              children:@[
            [EasyMenuItem itemWithTitle:@"子操作 1" image:nil handler:^{
                NSLog(@"子操作 1");
            }],
            [EasyMenuItem itemWithTitle:@"子操作 2" image:nil handler:^{
                NSLog(@"子操作 2");
            }]
        ]]
    ]];
    
    [button em_addMenuWithConfig:config];
}

兼容性检查

EasyMenu 内置了兼容性检查工具:

// 检查系统是否支持 EasyMenu
BOOL isSupported = [EasyMenuCompatibility isEasyMenuSupported];

// 获取系统版本信息
NSString *version = [EasyMenuCompatibility systemVersionInfo];

// 安全创建系统图标
UIImage *icon = [EasyMenuCompatibility safeSystemImageNamed:@"star"];

// 运行完整的兼容性测试
[EasyMenuDemo runCompatibilityTest];

最佳实践

  1. 兼容性: 使用 EasyMenuCompatibility 工具类安全创建系统图标
  2. 内存管理: EasyMenu 自动管理内存,无需手动处理
  3. 性能: 动态菜单的 provider block 应避免耗时操作
  4. 用户体验: 为菜单项提供合适的图标和描述性标题
  5. 🆕 预览功能: 预览视图控制器应该轻量级,避免复杂的网络请求
  6. 🆕 样式控制: 合理使用破坏性操作样式,仅用于删除等不可逆操作

常见问题

Q: 在 iOS 12 或更早版本上会发生什么? A: EasyMenu 会自动检测系统版本,在不支持的系统上不会添加菜单,并在控制台输出提示信息。

Q: 如何移除已添加的菜单? A: 调用 [view em_removeMenu] 即可。

Q: 动态菜单的性能如何? A: 动态 provider block 只在菜单即将显示时调用,性能影响很小。但建议避免在 block 中执行耗时操作。

Q: 预览功能在模拟器上能正常工作吗? A: 是的,预览功能在模拟器和真机上都能正常工作。在模拟器上可以通过长按来触发预览。

Q: 预览控制器会自动退出怎么办? A: 确保在 commitHandler 中使用 dispatch_async(dispatch_get_main_queue()) 并检查当前控制器状态,参考示例代码中的实现。

更新日志

v1.0.1 (2025-08-04)

  • 🆕 添加预览功能支持 (UIContextMenuInteraction Preview)
    • 长按预览:显示自定义预览视图控制器
    • 用力按压跳转:3D Touch/Haptic Touch 深度按压直接跳转
    • 支持配置级和菜单项级预览设置
  • 🆕 添加菜单样式控制功能
    • 菜单项状态控制:支持选中/未选中/混合状态
    • 菜单项属性控制:支持破坏性操作、禁用状态、隐藏等
    • 与系统原生样式完全一致
  • 🔧 修复预览跳转的生命周期管理问题
  • 📚 更新 Demo 应用,展示所有新功能
  • 📖 优化文档和使用示例

v1.0.0

  • 重构 API,简化使用方式
  • 添加 UIButton 和 UITableView 便捷扩展
  • 增强系统兼容性检查
  • 修复 actionProvider 返回类型错误

许可证

MIT License. 详见 LICENSE 文件。