布景

所谓EditMenu,便是如下图所示的菜单

iOS EditMenuInteraction组件

这样的作用,既能够自己完成也能够用体系供给的组件

iOS体系UIKit库供给的组件有UIMenuControllerUIEditMenuInteraction

UIEditMenuInteraction是iOS 16中引入的,从该版本开端UIMenuController被抛弃

本文所要讲的,便是根据体系的UIMenuControllerUIEditMenuInteraction封装了一个EditMenu款式的组件

为什么要封装组件

简单说,封装EditMenu便是运用前面说到的两个体系的EditMenu组件,封装成一个组件运用

为什么?原因很简单:

UIMenuController太难用,期望替换为UIEditMenuInteraction,但仍要兼容iOS 16之前的体系,又不想每个用到的当地都写两套代码

UIMenuController vs UIEditMenuInteraction

本小节经过讲解UIMenuController的运用办法和作业原理,比照与UIEditMenuInteraction的区别来说明为什么UIMenuController难用

网上搜一下会发现,有不少文章在讲怎样运用UIMenuController,且发现其间要留意的细节还不少。其实这现已从侧面说明这个组件不好用了(比照一下有多少文章来介绍怎样运用UILabelUIButton呢) UIMenuController的运用流程比较简单,如下:

  1. UIMenuController是个单例,直接获取实例
  2. 经过设置menuItems属性,配置额外的、需求显现的自定义的菜单选项(留意,是额外的选项,由于体系也会默许供给一些选项,如copy、paste等)
  3. 经过setTargetRect(_:in:)设置菜单显现方位
  4. 经过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

UITableViewDelegateUICollectionViewDelegate中也有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显现和操作的场景。比方下面这个场景

iOS EditMenuInteraction组件

iOS EditMenuInteraction组件

需求留意的是,以上API从iOS 14开端抛弃,取而代之的collectionView:contextMenuConfigurationForItemsAtIndexPaths:point:系列。留意新的API其实就不是EditMenu的款式了,而是上图所示的姿态(在苹果官方叫做ContextMenu)

EditMenuInteraction组件

根据上面分析的问题,咱们规划了EditMenuInteraction组件,它能够:

  • 封装了UIMenuControllerUIEditMenuInteraction的才能,所以兼容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编写,集成了UIMenuControllerUIEditMenuInteraction才能
  • EditMenuInteractionItem,Swift编写,表明菜单选项的数据源
  • EditMenuInteractionDummy,Objective C编写,组件内部私有类。经过OC Runtime的音讯转发机制完成无需新增菜单选项action的情况下仍能够显现期望的菜单选项意图

源码地址

觉得好用给点个star