源码

首页 » 归档 » 源码 » Hack 苹果系统 Api 实现 iOS TableViewCell 侧滑方案

Hack 苹果系统 Api 实现 iOS TableViewCell 侧滑方案


本篇不是多复杂的东西,只是苹果的官方 Api 支持能力不足,于是通过 Hack 的方案来补全苹果官方 Api ,但是每当新系统出来,iOS 内部实现经常发生变更,导致以前探索的 Hack 方案会失效,得重新绞尽脑汁 Hack,因此梳理留做记录

前言:

TableView Cell 的侧滑功能,一直是一个在 iOS 下比较有特色并且被广泛使用的一种交互

在日常的开发中也总会面临产品或者交互提出的种种侧滑定制的需求

image.png

早先的时候,侧滑相关的苹果官方 Api 的定制能力很差,但随着 iOS 的版本迭代,相关 Api 也在调整扩充中,逐步在开放更多定制能力,但即使是 iOS 12 系统 Api 也并不能做到很容易满足产品交互的定制需求。

因此在实现侧滑的定制效果上,始终存在这两种思路

  • 与 iOS UIKit Api 斗智斗勇,Trick & Hack 外加各种私有 Api 无所不用其极

  • 完全放弃 UIKit 的 canEdit 以及相关手势控制,完全自定义实现

与 UIKit 斗智斗勇

UITableView 的基本侧滑能力

开启编辑模式

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath

当这个 delegate 返回 YES 的时候,就算开启了当前 indexPath Cell 的编辑态,但也要注意编辑态包含2个模式

  • 左侧/右滑模式

该模式通过 [tableview setEditing:YES animated:YES]开启

image.png

  • 右侧/左滑模式

该模式左滑cell开启

image.png

只设置 canEditRowAtIndexPath 为 YES 的时候会默认同时开启左侧右侧2种模式,并且像上面的截图一样,左侧是一个红圈 - 号,右滑是删除按钮,并且点击左侧的 红圈 - 号 会自动左滑展现出右侧删除按钮

进入编辑模式

[tableview setEditing:YES animated:YES]

可以通过对整个 tableview 调用这个方法进入编辑模式,但注意这个方法只能进入左侧/右滑的编辑模式。并且会默认对所有 canEdit 的 indexPath 同时进入左侧/右滑编辑模式,看到红圈 - 号,如果想选定 cell 进入编辑模式,那就需要进一步控制 canEditRowAtIndexPath 这个 delegate 了。

想要进入右侧/左滑的编辑模式,只需要对 cell 进行左滑的手势操作就能进入。但如果不想通过用户的手势输入,而是通过代码的方式进入右侧/左滑的编辑模式则会很蛋疼,后面会讲到

提交编辑模式

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath

无论是左侧按钮还是右侧按钮,用户的点击操作都可以通过这个 delegate 来接收,通过 editingStyle 来区别来源 indexPath 来识别 cell

编辑模式样式状态

- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath

如果想进一步使用系统 Api 控制编辑状态,那么就可以通过上面的 delegate ,UITableViewCellEditingStyle 有三个值,但其实可以组合出4个状态

  • UITableViewCellEditingStyleNone

即便 canEdit 是 YES,当 CellEditingStyleNone 的时候,Cell 也不会允许你左滑手势出现右侧删除按钮,并且通过 tableview 的 setEditing 方法开启左侧红圈按钮的时候,你会发现 cell 确实向右滑动了,但红圈按钮已经消失,并且无法点击打开删除按钮

image.png

  • UITableViewCellEditingStyleDelete (这个状态是默认返回值)

这个状态允许左滑手势出现删除按钮,同时也允许 setEditing 出现红圈,红圈点击后出现删除按钮

  • UITableViewCellEditingStyleInsert

这个状态不允许左滑手势出现删除按钮,同时当 setEditing 的时候不出现红圈了,而是一个绿色 + 号,绿圈点击后不会出现删除按钮

image.png

  • UITableViewCellEditingStyleInsert | UITableViewCellEditingStyleDelete

这个状态就有意思了,Insert | Delete ,但这个状态下依然是不允许左滑手势出现删除按钮,区别是当 setEditing 的时候不出现红圈or绿圈了,而是一个复选框,复选框点击后是操作复选状态,不会出现删除按钮

image.png

编辑模式特殊样式状态 - 移动

- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath;

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath

上面2个 delegate 可以控制 TableView 是否可以进行 cell 的手势拖动,这里不进一步展开手势拖动如何实现,但 canMove 为 YES 后,会影响编辑模式的效果

image.png

如图所示,当 setEditing 后,cell 的右侧会出现一个 cell 可拖动的 icon ,此时整个cell的左滑删除手势已经彻底被禁止,当发生手势的时候,就会进入把 cell 拿起来的 move 模式

但 setEditing 的左侧按钮视 CellEditingStyle 不同,表现情况不变,当 CellEditingStyleDelete 时,点击红圈按钮,依然会自动左滑出现删除按钮(手势已被禁止,开启删除按钮可以通过这个方式)

- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath

在有些场景下,希望 setEditing 只进入纯粹的 move 模式,不想要左侧的按钮怎么办,可以通过这个 delegate ,这个 delegate 会在 canMove 模式下询问是否要进行 cell 左侧的偏移,默认返回 YES,如果设为 NO 就会不显示 cell 左侧按钮,cell也不偏移了

image.png

这个 delegate 有着很苛刻的限制条件

  • CellEditingStyle 必须为 CellEditingStyleNone

只有 CellEditingStyleNone 的时候才可以实现 cell 左侧按钮不显示,cell也不偏移

PS:不要妄想通过这个 delegate 禁掉 cell 的左侧编辑按钮,从而让 setEditing 直接进入右侧按钮展现模式 ╮(╯_╰)╭

右侧按钮的Title定制

- (nullable NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath

在早先版本(iOS 8以前)苹果并未开放多右侧按钮的 Api ,因此右侧按钮几乎就是为了删除而定制的,因此苹果给这种右侧按钮的 style 命名为 CellEditingStyleDelete

也因此苹果只提供了一个 delegate 来定制这个按钮的文案

这个 delegate 会让右侧按钮自适应文字的宽度

UITableView 深度定制侧滑按钮(iOS 11以前)

很明显,侧滑按钮只允许有一个,只能换文字,不能换颜色,不能加图表,这么多限制说出去,告诉产品和交互我们实现不了,这肯定会被拍死,于是乎,iOS 8 之后,苹果开放了更多定制

- (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath
{
    WEAKSELF(self)
    UITableViewRowAction *delAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:@"删除" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) {
        // todo xxxxx
        
......

        NSLog(@"点击了删除");
        tableView.editing = NO;
    }];
    delAction.backgroundColor = [UIColor colorWithRGB:0xF4333C];
UITableViewRowAction *hahaAction = [UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleNormal title:@"哈哈" handler:^(UITableViewRowAction *action, NSIndexPath *indexPath) {
        // todo xxxxx
        
......

NSLog(@"点击了哈哈");
        tableView.editing = NO;
    }];
    hahaAction.backgroundColor = [UIColor colorWithRGB:0x108EE9];
    
return @[delAction,hahaAction]
}

终于苹果开放了这个新的 Api ,可以支持向右侧设置多个按钮,并且每个 UITableViewRowAction 就代表着一个按钮,可以设置 rowActionWithStyle (3个枚举值是改变按钮配色),可以设置 title ,可以设置 backgroundColor。没错就只能定制这么多。

交互说要有 icon !

交互说要有 icon !

交互说要有 icon !

重要的事情说三遍

Hack 系统 UIView 层级,图标/字体等深度定制

我们先分析一下 UITableView Cell 的 UIView 层级

image.png

从图中可以看出来右滑按钮的界面层级是

iOS 8 - iOS 10

  • UITableView

  • UITableViewCell

  • UITableViewCellContentView (我们熟知的 cell 的内容 view)

  • UITableViewCellDeleteConfirmationView

  • UITableViewCellActionButton (侧滑按钮)

那么我们可以在恰当的时机 layoutSubview 的时候,获取到 UITableViewCell ,遍历他的 subview 找到 _UITableViewCellActionButton 这个 Button 类,进行人为的修改-(void)layoutSubviews{

    [super layoutSubviews];
    for (UIView *subView in self.subviews) {
        if ([subView isKindOfClass:NSClassFromString(@"UITableViewCellDeleteConfirmationView")]) {
            for (UIButton *btn in subView.subviews) {
                if ([btn isKindOfClass:[UIButton class]]) {
// 很setImage好,你已经拿到了这个btn了,可以为所欲为了
// todo
                }
            }
        }
    }
}

这种遍历视图层级的方法在 iOS 11 以后有问题!

这种遍历视图层级的方法在 iOS 11 以后有问题!

这种遍历视图层级的方法在 iOS 11 以后有问题!

重要的事情说三遍

为所欲为:

需要说明的是,简单的直接操作btn,可以对btn调用下面这些 Api 来进行 icon 的 UI 定制

  • btn.titleLabel setFont:

  • btn setTitleColor:forState:

  • btn setImage:forState:

但是直接这样设置,会导致 image 与文字并排出现,位于文字左边,并且有一定的重叠,这时候你可能会想到下面这俩 Api

  • btn.titleEdgeInsets

  • btn.imageEdgeInsets

但实际操作后你会发现,imageEdgeInsets 可以正常的工作,但是 title 的位置始终不能如你所愿按你的意愿进行更改,通过对 titleLabel 写个监听可以发现,layoutSubview 之后,系统还干了别的事情,重新进行了校正,所以想要更改 title 的位置有两个方案

  • 对 title 的 frame 进行 kvc 监听,从而在系统坐标校正后,改回你想要的位置

  • dispatch_after 个0.1秒,再手动修改 title 的 frame (╮(╯_╰)╭)

image.png

为所欲为2.0:

如果交互与设计想要这个页面更加花哨,更加风骚,更加的定制化怎么办?

整个 btn 都给你了,你可以完全构造一个 customView ,frame 大小与 button 完全一致,直接盖在 btn 上,然后这个页面就彻底为所欲为了,想加什么 UI 加什么 UI,但你需要注意以下几个事情

  • btn 的 width 是由 UITableViewRowAction 的 Title 文字来决定的

  • Title 会被系统维护在居中的位置,你盖在上面的 customView 可以考虑 center 与 Title保持一致

调用私有 Api 动态展现/收起左滑动画

交互来提新需求了,交互说怕有人不知道可以左滑,专门设计一个按钮 ... 点了这个按钮,自动帮用户左滑出来。总结一下需求是:希望不用通过用户的左滑手势,就能让cell出现动画,左滑滚动出右侧按钮

寻遍所有 tableview & tableview cell 的 Api 都没有发现可以通过代码触发 cell 左滑,从而展现右侧按钮的方法。唯一比较接近的方法是 [tableview setEditing:YES animated:YES]。但这个方法有个问题在上文提到过,setEditing 会第一次进入右滑并展现左侧红圈按钮的状态,只有再次点击红圈按钮,才能进入左滑展现右侧按钮。换句话说,我可以用 setEditing:NO 来收起左滑动画关闭编辑模式,但展现左滑还需想办法。

各种查找方案后发现系统有2个私有 Api 可以实现,先对 tableview 调用 _endSwipeToDeleteRowDidDelete 传入 NO 来让整个 tableview 因为 trick 调用导致的状态清零,然后在对特定的 cell 调用 setShowingDeleteConfirmation 传入 YES 来让 cell 进行左滑动画,从而展现右侧按钮。

  • 展现左滑动画:2个私有 Api 的调用

  • 收起左滑动画:setEditing:NO

补充:既然是私有 Api ,如何调用自然是 runtime 了,不管你是 perfermSelector 还是 Object_msgSend 怎么具体的调用我就不细说了,但千万要注意一定要把 selector 的字符串进行混淆

这个方法在 iOS 11 以后有问题!

这个方法在 iOS 11 以后有问题!

这个方法在 iOS 11 以后有问题!

重要的事情说三遍

UITableView 深度定制侧滑按钮(iOS 11以后)

- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView leadingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath 
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath

苹果仿佛听到了群众的呼声,群众对侧滑按钮上面加 icon 的呼声,于是在 iOS 11 之后加入了新的 Api 来解决这个问题,而且还特别开心的一口气开放了2,支持了左滑和右滑的2种手势,简单的实现如下

[tableview setEditing:YES animated:YES]

0

那么问题来了

  • UIContextualAction 和 iOS 10 以前的 UITableViewRowAction 他俩是什么关系?可以共存么?

  • leadingSwipeActionsConfigurationForRowAtIndexPath 是从左往右滑漏出左边的侧滑按钮,那么和 setEditing 出现的左侧红圈按钮,他俩是什么关系?可以共存么?

iOS 11 新老 Api 共存关系

整个 UISwipeActionsConfiguration 的 iOS 11 新 Api 都是可以和过去的 setEditing & UITableViewRowAction 共存,只不过苹果推荐使用新 Api 老的 UITableViewRowAction 在未来可能会被抛弃,我们来看一下他俩的共存状态。

让我们同时用2个 delegate 实现侧滑 删除 按钮 哈哈 按钮,只不过老 UITableViewRowAction 用的是红灰配色,新 UISwipeActionsConfiguration 用的是红蓝配色

  • 当我们直接用手势进行左滑操作

  • cell 出现了红蓝配色的侧滑按钮

image.png

  • 我们在 didselect 的时候 进行 setEditing:YES

  • cell 出现了左侧 红圈按钮

  • 点击红圈按钮 cell 出现了红灰配色的侧滑按钮

image.png

其实大家还可以把 trailingSwipe 换成 leadingSwipe 的右滑,来看看 leadingSwipe 与 setEditing 的 红圈按钮 的冲突情况,我就不详细展开了

总结如下:

  • 当实现了 leadingSwipe or trailingSwipe 的新 Api delegate 后

    无论左滑右滑,都会执行新 Api 的 UISwipeActionsConfiguration 结果

  • 当没有实现新 Api,于是只走旧 editActionsForRowAtIndexPath 的 Api

    展现 UITableViewRowAction 的结果

  • 当操作 setEditing:YES 后

    cell 会遵循旧 editActionsForRowAtIndexPath 与 UITableViewCellEditingStyle 进行展现

Hack 系统 UIView 层级 新-为所欲为

新 Api 确实提供了增加 icon 的方法,但通过上面的截图发现,icon 与 title 的位置还是不够自由,更何况我们还有更深度的任意定制交互的需求,所以我们依然需要为所欲为,然而!旧的为所欲为方案不管用了。

老办法,分析一波系统 View 层级

image.png

新的系统 View 层级是这样的

  • UITableView

  • UITableViewCell

  • UITableViewCellContentView

  • UISwipeActionPullView

  • UISwipeActionStandardButton

于是问题找到了,之前在 cell 层级之下的用来放置侧滑按钮的 UITableViewCellDeleteConfirmationView 已经没了,新的侧滑按钮并没有放在 cell 层级之下,而是提升到了 cell 的平级,tableview 之下了,新的类名叫 UISwipeActionPullView,并且无论你用的新 Api 还是老 Api ,侧滑按钮的层级都是这样,所以以前那套 Hack 查找 View 的方法在 iOS 11 不适用了,得更新了。

首先不能再 tableview cell 的 layoutSubview 来进行这个操作了,而是应该换到 tableview 的 layoutSubview 中进行

[tableview setEditing:YES animated:YES]

1

为所欲为3.0

嗯,你已经找到 button 了,又可以为所欲为了

动态展现/收起左滑动画 (老办法私有 Api 失效…

看来 iOS 11 对 tableview 内部侧滑按钮的方案改动还真是非常大,不仅仅界面层级失效了,连私有 Api 动态展现侧滑按钮都失败了。想尽一切办法,各种搜索,有且仅有一篇文章可以勉强实现,并且还有瑕疵。

点击Cell上按钮出现Cell的左滑删除编辑界面

大家还记得我在文章中反复提到的左侧 红圈按钮 么?

image.png

这篇文章的思路其实说的有点多,如果去掉一些细枝末节的逻辑,简化版就是如下(这思路真是野…)

  • setEditing: YES 让 cell 进入编辑态,然后同步进入下一步

  • 通过查找 cell 的 UITableViewCellEditControl 类找到红圈button

  • 直接调用这个 button 的 sendActionsForControlEvents 代码触发点击事件

  • setEditing: NO 恢复常态

[tableview setEditing:YES animated:YES]

2

这个办法目前为止还存在问题,如何在点击后收回侧滑按钮回复常态,这个问题即便是文章原文也并没有解决:

  • 如果通过这种方法展示了左滑动画,漏出右侧按钮

  • 用户再次点击 tableview cell 的时候,不会触发任何 cell 的 contentview 的点击事件,也不会触发 didselect (本意是想在这些事件里执行 setEditing: NO)

  • 而是会直接进入系统操作,右滑漏出左侧红圈按钮

  • 用户不再次点击 tableview cell,而是用户在手动右滑,把cell滑回去

  • 无法做到恢复 setEditing: NO 的状态,而是右滑漏出左侧 红圈按钮

其实我们需要的就是一个时机,来让代码执行 setEditing: NO 就可以收回侧滑按钮,回归正常 cell 状态,但眼下根本无法捕获这个时机,cell 的didselect,用户的点击触摸都无效。

为什么会这样我们分析一下原因,所有的问题都发生在 左滑动画,漏出右侧按钮 这个状态,所以在这个状态下我们观察一下 cell 的视图层级

image.png

纳尼!TableViewCell 的 userInteractionEnabled 居然在这种状态下是 NO !我们写个 KVO 来观察一下是不是真的这样,如果是,我再把他置回来

[tableview setEditing:YES animated:YES]

3

试了一下果然!只要进入 左滑动画,漏出右侧按钮 都会监听到系统设置 cell 的 userInteraction 为 NO ,然而我还是想简单了,即便我把他设置回 YES ,依然没有任何的卵用(鬼知道苹果的程序员里面写的是怎么样的代码),即便我设置了 YES ,整个 cell 依然接受不到任何的touch事件

  • cell 重写了 touchBegin 发现在这个状态下不会被调用,即便 userInteraction 为 YES

  • tableview 重写了 touchBegin 发现在这个状态下不会被调用,即便 userInteraction 为 YES

还得另想办法,touch 这条路走不通换了一个思路观察一下手势,因为在 iOS 的触摸事件中,手势识别与触摸响应,是2个不同的流程。于是我在 cell 的类里重写了 UIGestureRecognizerDelegate 的所有 delegate ,只为希望通过这么些 delegate 来观察到底系统的 cell 中的手势是怎么做的,这些 delegate 能否呈现一些蛛丝马迹,结果发现,有且只有2个 delegate 可以被响应

[tableview setEditing:YES animated:YES]

4

在 gestureRecognizer:shouldReceiveTouch: 中,只会传递出一个 UILongPressGesture,但是我强制 return YES,依然没效果,Touch不触发

在 gestureRecognizer:shouldRequireFailureOfGestureRecognizer: 中我们可以看到有无数个 gesture 他们之间被相互调用来询问依赖关系,而最可疑的还是那个 UILongPressGesture

并且还有一个现象,在 左滑动画,漏出右侧按钮 的状态下,无论是点击,还是滑动,都会触发相同的 UILongPressGesture 手势 shouldReceiveTouch 询问,好吧我就觉得这个最可疑,就在这个时机尝试直接调用 setEditing: NO 试试吧,谁知道呢,就靠猜了

[tableview setEditing:YES animated:YES]

5

居然。。。成功了!!!! 回顾一下实现过程

  • 通过代码触发 cell 左滑动画,漏出侧边按钮

    先让 cell 进入 Editing 状态

    再遍历 cell subview 找到 UITableViewCellEditControl

    直接代码触发 UITableViewCellEditControl 的点击事件

  • 通过代码触发 cell 回复正常,收起侧边按钮

    先用 KVO 监听 cell 的 userInteractionEnabled

    在系统置 userInteraction 为 NO 后,重置为 YES

    再用手势 gestureRecognizer:shouldReceiveTouch: 监听系统手势询问

    在 UILongPressGesture 询问 shouldReceiveTouch 的时候 setEditing:NO

自行实现滑动手势 tableview cell

怎么样,是不是有点够够的了?

  • 无止境的通过页面层级去干预系统界面

  • 没目标的尝试各种 KVO 来猜测系统内部代码

  • 无头绪的重写 cell 的全部手势 delegate 来猜测系统到底有多少种手势

  • 在不知道保险不保险的时机,非正常的调用系统 Api

  • 受够了因为苹果的 iOS 版本迭代,导致所有的 Hack 尝试手段失效,重新猜测适配

还是自己实现吧,彻底丢掉苹果官方 Api 的依赖,自己实现吧,这样就没了很多苹果限定的约束,虽然麻烦点,但好处就可以满足任何需求的定制了。

大概的思路类似于这样,我就不深入展开了,因为相关的开源库都很多,一千个人有一千种设计与写法

  • 放弃 tableview 自带的编辑模式,canEdit 返回 NO,自己实现 custom Edit

  • 对 cell 加一个 pan 手势识别

  • 在 UIGestureRecognizerStateBegan 的时候,根据 delegate 设计构造出 swipeView 和支持定制的子 button

  • 在 UIGestureRecognizerStateChanged ,来精确计算手指移动过的位移 deltaX ,从而运算出 swipeView 的约束与原本 cell 里面的 UI 新的约束,从而随着手指移动刷新界面元素

  • 在 UIGestureRecognizerStateEnd 的时候,根据界面元素的位置,算出应该回弹的距离,用动画作出回弹效果

(0)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/hack-pingguoxitong-api-shixian-ios-tableviewcell-cehuafangan.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:11 月 14, 2018 at 03:36 下午

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复