EasyMenu
是一个基于 UIMenu
和 UIContextMenuInteraction
的轻量级封装,为任何 UIView
提供极其简单、易于集成的上下文菜单解决方案,支持预览功能和丰富的菜单样式控制。
- 长按预览:显示自定义预览视图控制器
- 用力按压跳转:3D Touch/Haptic Touch 深度按压直接跳转到详情页面
- 无缝体验:预览到跳转的平滑过渡动画
- 菜单项状态:支持选中/未选中/混合状态显示
- 样式属性:破坏性操作(红色)、禁用状态(灰色不可点击)
- 视觉反馈:与系统原生菜单样式完全一致
在 iOS 13 之后,苹果引入了 UIMenu
,它提供了一种现代化的方式来展示上下文菜单。然而,直接使用 UIContextMenuInteraction
和其代理方法相对繁琐,尤其是在需要为多个视图或动态内容(如 UITableViewCell
)提供菜单时。
EasyMenu
的诞生就是为了解决这个问题。它通过 UIView
的 Category 扩展,提供简洁的 API,让你只需几行代码就能为任何视图添加功能强大的静态或动态菜单,同时自动处理了所有底层的代理和生命周期管理。
- 简洁 API: 使用配置类
EasyMenuConfig
和EasyMenuItem
,以声明式的方式构建菜单 - 🆕 预览功能: 支持长按预览和用力按压跳转,提供类似系统照片应用的体验
- 🆕 样式控制: 支持菜单项状态(选中/禁用)和样式(破坏性操作)控制
- 静态与动态: 支持一次性配置的静态菜单和根据上下文动态生成的菜单
- 自动管理: 无需手动处理
UIContextMenuInteraction
的创建、添加和代理设置 - 轻量级: 无任何第三方依赖,仅包含几个核心文件,易于集成
- 子菜单支持: 轻松创建包含层级关系的子菜单
- 系统兼容性: 内置 iOS 版本检查,安全处理系统图标兼容性
- 便捷扩展: 针对 UIButton 和 UITableView 提供专门的便捷方法
- iOS 13.0+
- Xcode 11.0+
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"
// 为视图添加静态菜单
- (void)em_addMenuWithConfig:(EasyMenuConfig *)config;
// 为视图添加动态菜单
- (void)em_addMenuWithDynamicProvider:(DynamicMenuProvider)provider;
// 移除菜单
- (void)em_removeMenu;
// 创建带预览功能的菜单项
+ (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;
// 创建带样式的菜单项
+ (instancetype)itemWithTitle:(NSString *)title
image:(nullable UIImage *)image
handler:(nullable void (^)(void))handler
state:(UIMenuElementState)state
attributes:(UIMenuElementAttributes)attributes;
// 添加常用菜单(复制、分享、自定义操作)
- (void)em_addCommonMenuWithCopyText:(nullable NSString *)copyText
shareObject:(nullable id)shareObject
customActions:(nullable NSArray<EasyMenuItem *> *)customActions;
// 为表格启用上下文菜单
- (void)em_enableContextMenuWithEditHandler:(nullable void(^)(NSIndexPath *indexPath))editHandler
deleteHandler:(nullable void(^)(NSIndexPath *indexPath))deleteHandler
shareHandler:(nullable void(^)(NSIndexPath *indexPath, id data))shareHandler;
#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];
}
- (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];
}
- (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];
}
- (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(@"显示时间信息");
}]
]];
}];
}
- (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];
- 兼容性: 使用
EasyMenuCompatibility
工具类安全创建系统图标 - 内存管理: EasyMenu 自动管理内存,无需手动处理
- 性能: 动态菜单的 provider block 应避免耗时操作
- 用户体验: 为菜单项提供合适的图标和描述性标题
- 🆕 预览功能: 预览视图控制器应该轻量级,避免复杂的网络请求
- 🆕 样式控制: 合理使用破坏性操作样式,仅用于删除等不可逆操作
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())
并检查当前控制器状态,参考示例代码中的实现。
- 🆕 添加预览功能支持 (UIContextMenuInteraction Preview)
- 长按预览:显示自定义预览视图控制器
- 用力按压跳转:3D Touch/Haptic Touch 深度按压直接跳转
- 支持配置级和菜单项级预览设置
- 🆕 添加菜单样式控制功能
- 菜单项状态控制:支持选中/未选中/混合状态
- 菜单项属性控制:支持破坏性操作、禁用状态、隐藏等
- 与系统原生样式完全一致
- 🔧 修复预览跳转的生命周期管理问题
- 📚 更新 Demo 应用,展示所有新功能
- 📖 优化文档和使用示例
- 重构 API,简化使用方式
- 添加 UIButton 和 UITableView 便捷扩展
- 增强系统兼容性检查
- 修复 actionProvider 返回类型错误
MIT License. 详见 LICENSE 文件。