布景
所谓EditMenu,便是如下图所示的菜单
这样的作用,既能够自己完成也能够用体系供给的组件
iOS体系UIKit库供给的组件有UIMenuController
和UIEditMenuInteraction
UIEditMenuInteraction
是iOS 16中引入的,从该版本开端UIMenuController
被抛弃
本文所要讲的,便是根据体系的UIMenuController
和UIEditMenuInteraction
封装了一个EditMenu款式的组件
为什么要封装组件
简单说,封装EditMenu便是运用前面说到的两个体系的EditMenu组件,封装成一个组件运用
为什么?原因很简单:
UIMenuController太难用,期望替换为UIEditMenuInteraction
,但仍要兼容iOS 16之前的体系,又不想每个用到的当地都写两套代码
UIMenuController vs UIEditMenuInteraction
本小节经过讲解UIMenuController
的运用办法和作业原理,比照与UIEditMenuInteraction
的区别来说明为什么UIMenuController
难用
网上搜一下会发现,有不少文章在讲怎样运用UIMenuController
,且发现其间要留意的细节还不少。其实这现已从侧面说明这个组件不好用了(比照一下有多少文章来介绍怎样运用UILabel
、UIButton
呢)
UIMenuController
的运用流程比较简单,如下:
- UIMenuController是个单例,直接获取实例
- 经过设置menuItems属性,配置额外的、需求显现的自定义的菜单选项(留意,是额外的选项,由于体系也会默许供给一些选项,如copy、paste等)
- 经过setTargetRect(_:in:)设置菜单显现方位
- 经过setMenuVisible(_:animated:)显现、躲藏菜单
难用的当地,同时也是网上问的最多的是:
- 菜单无法正常显现
- 菜单显现的选项包括了不期望呈现的体系供给的选项
这两个问题的原因是一个:UIMenuController
决议显现哪些选项的原理不易了解
UIMenuController怎么决议显现哪些选项
最要害的是:UIMenuController
经过问询UIResponder
对象构成的responder chain来决议终究显现的选项
咱们幻想在一个谈天页面中,需求长按某条音讯显现菜单选项的场景,经过该场景简述一下UIMenuController的作业原理:
- Responder Chain大致是这样:UIlabel -> UIView(cell.contentView) -> UICollectionViewCell -> UICollectionView -> UIView->UIViewController
- 履行
setTargetRect(_:in:)
办法时,in参数传的是最上层的UILabel - 那么,当履行
setMenuVisible(_:animated:)
时,关于Responder Chain中的每个对象,体系都会经过canPerformAction(_:withSender:)
来确定某个action(Selector)能否被处理,假如回来true,则终究会显现出来 - 但假如false,并不意味着一定不会显现。由于只要有一个responder回来true,终究就会显现,只要一切responder都回来false,才不会显现
正是这样的规划—单例+多个数据源(多个responder)决议终究的状态,导致不易用且难以调试
- 单例,意味着多个场景下运用一份组件和数据,那么在场景1中决议哪些菜单要显现时,还得考虑其他场景的菜单选项会不会搅扰到场景1
- 多数据源决议一个状态,多一个数据源就添加问题复杂度,调试复杂度
这还不算完
canPerformAction(:withSender:)有自己的默许完成:假如当前UIResponder完成了该办法参数中说到的action对应的办法,则回来true,否则继续履行nextResponder.canPerformAction(:withSender:)
- 仍是前面的Responder Chain,假如只是履行
setTargetRect(_:in:)
和setMenuVisible(_:animated:)
,咱们根本看不到任何菜单选项呈现。由于整个Responder Chain的canPerformAction(_:withSender:)
都回来false - 为了能够显现,咱们不得不在
UICollectionViewCell
中添加了每个菜单action对应的办法完成 - 很可能终究的事情处理要在
UIViewController
中处理,所以每个action的完成中,要经过delegate等办法将事情传递到UIViewController
中
以上,便是UIMenuController
作业原理的解释,总结一下:
- 从API角度来看,运用不复杂;但真的运用起来,略微复杂一些的场景,就很简单呈现显现不出来的问题
- 只要对它的作业原理有熟悉的了解后,才干不易出错。(其实体系还会履行
UIResponder.target(forAction:withSender:)
,运用复杂度还会进一步添加) - 文中没有展示由于单例同享数据带来的问题,其实实践开发中是遇到过的。比方谈天页面场景下,文本输入框中能够长按呈现菜单,长按音讯也能够呈现菜单,两边场景下的菜单选项不同,但其实都存在同一单例中,是会有影响的
UIEditMenuInteraction
反观新的体系组件-UIEditMenuInteraction
,规划就好用很多
- 不是单例,哪里需求哪里创建。不必考虑其他场景对当下的影响
- 不必遍历多个数据源(UIResponder)来决议展示哪些菜单,为
UIEditMenuInteraction
实例供给哪些菜单,终究就显现哪些 - 不需求在UIResponder供给action的默许完成进行事情处理,事情处理一致在回调中
EditMenu in UITableViewDelegate or UICollectionViewDelegate
UITableViewDelegate
和UICollectionViewDelegate
中也有EditMenu相关的办法,以UICollectionView
为例
- (BOOL)collectionView:(UICollectionView *)collectionView shouldShowMenuForItemAtIndexPath:(NSIndexPath *)indexPath API_DEPRECATED_WITH_REPLACEMENT("collectionView:contextMenuConfigurationForItemsAtIndexPaths:point:", ios(6.0, 13.0));
- (BOOL)collectionView:(UICollectionView *)collectionView canPerformAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender API_DEPRECATED_WITH_REPLACEMENT("collectionView:contextMenuConfigurationForItemsAtIndexPaths:point:", ios(6.0, 13.0));
- (void)collectionView:(UICollectionView *)collectionView performAction:(SEL)action forItemAtIndexPath:(NSIndexPath *)indexPath withSender:(nullable id)sender API_DEPRECATED_WITH_REPLACEMENT("collectionView:contextMenuConfigurationForItemsAtIndexPaths:point:", ios(6.0, 13.0));
依据测试发现,
- 在iOS 16之前,经过上述
UICollectionViewDelegate
的API完成的EditMenu作用,本质上体系仍是经过UIMenuController
来显现EditMenu - iOS 16中,则是运用的
UIEditMenuInteraction
另外,最重要的一点是:
不管iOS 16之前仍是iOS 16版本,以上API都不太简单完成自定义EditMenu要显现的方位。EditMenu的显现方位是根据整个Cell的尺度和方位,并由体系来控制。所以以上API的适用场景是,对整个Cell进行Menu显现和操作的场景。比方下面这个场景
需求留意的是,以上API从iOS 14开端抛弃,取而代之的collectionView:contextMenuConfigurationForItemsAtIndexPaths:point:系列。留意新的API其实就不是EditMenu的款式了,而是上图所示的姿态(在苹果官方叫做ContextMenu)
EditMenuInteraction组件
根据上面分析的问题,咱们规划了EditMenuInteraction
组件,它能够:
- 封装了
UIMenuController
和UIEditMenuInteraction
的才能,所以兼容iOS 16之前和之后的体系 - 一致了输入数据源和事情回调逻辑,处理冗余代码问题,进步易用性
- 由于这两个体系组件的输入(即菜单选项)和事情回调处理逻辑各不相同,假如项目中多处用到EditMenu款式,那输入和事情回调处理就要写多次,代码冗余
- 无需编写不易了解的Action办法。运用
UIMenuController
时,大概率要去完成每个菜单选项对应的actioin办法,这并不易用
运用办法
- (void)collectionView:(UICollectionView *)collectionView didLongPress:(Model *)msg indexPath:(NSIndexPath *)indexPath {
NSArray<MessageCellMenuItem *> *cellMenuItems = [self menuItemsForIndexPath:indexPath];
NSArray<EditMenuInteractionItem *> *menuItems = [self editMenuItemsWithCellMenuItems:cellMenuItems indexPath:indexPath];
MessageCell *cell = (MessageCell *)[collectionView cellForItemAtIndexPath:indexPath];
CGRect targetRect = [cell.contentView convertRect:cell.messageContainerView.frame toView:cell];
[self.menuInteraction showMenu:menuItems targetRect:targetRect for:cell];
}
- (NSArray<EditMenuInteractionItem *> *)editMenuItemsWithCellMenuItems:(NSArray<MessageCellMenuItem *> *)cellMenuItems
indexPath:(NSIndexPath *)indexPath {
NSMutableArray<EditMenuInteractionItem *> *items = [NSMutableArray array];
for (MessageCellMenuItem *cellItem in cellMenuItems) {
EditMenuInteractionItem *item = [[EditMenuInteractionItem alloc] initWithTitle:cellItem.title callback:nil];
@weakify(self);
switch (cellItem.type) {
case MessageCellMenuTypeCopy: {
item.callback = ^{ [weak_self copyMsgAtIndexPath:indexPath]; };
break;
}
case MessageCellMenuTypeDelete: {
item.callback = ^{ [weak_self deleteMsgAtIndexPath:indexPath]; };
break;
}
}
[items addObject:item];
}
return items;
}
-
(void)collectionView:didLongPress:indexPath:
办法是collectionviewcell长按时的回调 -
(NSArray<EditMenuInteractionItem > *)editMenuItemsWithCellMenuItems:indexPath:
办法,用于构建EditMenuInteraction
所需求的菜单选项,仅有两个信息:title和callback -
[self.menuInteraction showMenu:menuItems targetRect:targetRect for:cell]
,显现菜单选项- for参数表明要在哪个视图显现菜单选项
- targetRect用于控制菜单选项的方位,比方长按一条谈天音讯时,能够传入表明文本的label的rect
- 留意:targetRect是根据for参数中的视图的坐标系的
源码
源码包括三个类:
-
EditMenuInteraction
,核心类,Swift编写,集成了UIMenuController
和UIEditMenuInteraction
才能 -
EditMenuInteractionItem
,Swift编写,表明菜单选项的数据源 -
EditMenuInteractionDummy
,Objective C编写,组件内部私有类。经过OC Runtime的音讯转发机制完成无需新增菜单选项action的情况下仍能够显现期望的菜单选项意图
源码地址
觉得好用给点个star