仿微信左滑删除
Demo地址:https://github.com/SPStore/WeChatDelete
开门见山,先上微信原生效果图
这个效果也只有从iOS11开始,微信才有的,iOS11之前点击删除,底部会弹出一个是否确认删除的提示框,既然是iOS11才有,那么微信必然用了iOS11的新特性。这个功能实现起来非常非常简单,不用自定义cell,一个UILabel就可以搞定,虽然简单,但是想到这个方案的过程当中遇到了许多阻碍,我会一个一个为大家排解。
切入正题
在iOS11 以后,我们要实现左滑删除功能,方法如下:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath API_AVAILABLE(ios(11.0)){
UIContextualAction *deleteRowAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:[NSString stringWithFormat:@"删除"] handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
}];
UIContextualAction *remarkAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"备注" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
}];
UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[deleteRowAction,remarkAction]];
config.performsFirstActionWithFullSwipe = NO;
return config;
}
好的,我们已经实现了左滑出现两个按钮,一个是备注,一个是删除,如图:
我们渲染一下层级结构图,发现iOS11之后层级如下:
截图中红方框款起来的就是左滑按钮的层级结构,发现有一个
UISwipeActionPullView
,这个view加在了UITableView上,该view有1个子控件:UISwipeActionStandardButton
,在这个 button里,系统插入了一个UIView,我猜想这个view有2个功能:一个是方便添加毛玻璃效果,一个是要实现系统的使劲左滑后action变长效果(备注:在iOS11之前,左滑按钮是加在cell上的)。
废话
首先,我想大家和我一样,实现这个微信左滑删除效果,第一个想到的,就是在点击删除按钮的block块当中,改变删除action的标题,将其title改为“确认删除”,但是很遗憾,没有用,你改变之后,系统内部会再重置一次,会覆盖掉你的修改,既然系统会重置,那么我就想,我用GCD函数dispatch_after延时0.1秒修改呢,这样就会先走系统的修改,再走我的修改,这样不就能实现了吗?是的,的确如我所料,延时0.1秒能修改成功,但是,修改为“确认删除”文字后,当你的手指按下“确认删除”按钮的那一刻,会瞬间变一下“删除”,然后再变回“确认删除”,所以此路行不通,而且这样做,最多能修改文字,不能修改“删除”按钮的宽度。
我们仔细研究一个重要问题:
当我们在创建一个action的时候,是这样创建的:
UIContextualAction *deleteRowAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:[NSString stringWithFormat:@"删除"] handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
}];
其中有一个参数是block,这个block就是本文的重点研究对象,我们仔细看看这个block,有3个参数:
参数1:action,这个参数就是创建的action对象
参数2:sourceView,这个view非常重要,当action的title不为空时,sourceView是一个UILabel, 当action的title为空时,sourceView是一个UIButton(实际上是UISwipeActionStandardButton),那既然控件都给我们了,我想大家肯定也是聪明人,在这个地方必有文章可做。
参数3 :completionHandler,这个参数是一个block,这个block是一个细节了,不知道大家有没有注意到,在iOS11之前,只要你点击了左滑出现的任意一个按钮,cell都会退出编辑,也就是左滑按钮会消失,iOS11之后不会了,如果你想实现这个效果,只要回调一下completionHandler即可,参数是一个 BOOL 值,传YES和NO的区别是:传NO,系统只退出编辑,传YES ,如果是删除样式,系统会自动为你做删除cell操作。
3个参数讲完了,我们把重点放在第二个参数sourceView上
解决方案:
我的思路是,创建一个UILabel,点击删除按钮时,将该Label加在sourceView最顶层父view上,即加在前面提到过的UISwipeActionPullView
上,同时以UIView动画改变这个Label的x值和width,核心源码如下:
// 先创建一个UILabel
- (UILabel *)sureDeleteLabel {
if (!_sureDeleteLabel) {
UILabel *sureDeleteLabel = [[UILabel alloc] init];
sureDeleteLabel.text = @"确认删除";
sureDeleteLabel.textAlignment = NSTextAlignmentCenter;
sureDeleteLabel.textColor = [UIColor whiteColor];
sureDeleteLabel.backgroundColor = [UIColor colorWithRed:255.0/255.0 green:56.0/255.0 blue:50.0/255.0 alpha:1.0];
sureDeleteLabel.userInteractionEnabled = YES;
_sureDeleteLabel = sureDeleteLabel;
}
return _sureDeleteLabel;
}
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath API_AVAILABLE(ios(11.0)){
UIContextualAction *deleteRowAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:[NSString stringWithFormat:@"删除"] handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
// 核心代码
UIView *rootView = nil; // 这个根view指的是UISwipeActionPullView,最上层的父view
if ([sourceView isKindOfClass:[UILabel class]]) {
rootView = sourceView.superview.superview;
self.sureDeleteLabel.font = ((UILabel *)sourceView).font;
}
self.sureDeleteLabel.frame = CGRectMake(sourceView.bounds.size.width, 0, sourceView.bounds.size.width, sourceView.bounds.size.height);
[sourceView.superview.superview addSubview:self.sureDeleteLabel];
[UIView animateWithDuration:0.7 delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:1 options:UIViewAnimationOptionCurveEaseInOut animations:^{
CGRect labelFrame = self.sureDeleteLabel.frame;
labelFrame.origin.x = 0;
labelFrame.size.width = rootView.bounds.size.width;
self.sureDeleteLabel.frame = labelFrame;
} completion:^(BOOL finished) {
}];
}];
UIContextualAction *remarkAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"备注" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
}];
UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[deleteRowAction,remarkAction]];
config.performsFirstActionWithFullSwipe = NO;
return config;
}
到这里,基本的效果已经实现了,但是点击事件又成了一个很头疼的问题,我们要点击 确认删除
响应我们的点击事件呀,但是造化弄人,你在确认删除
Label上加一个tap手势,即便交互被打开,这个tap手势事件并不会被触发,即便把UILabel换成UIButton也不会触发按钮点击事件,触发的依然是系统自带的删除
按钮事件和备注
事件,这个地方我想了很久,系统一定是重写了UISwipeActionPullView
的- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
方法,在这个方法对开发者自主添加的控件做了过滤处理
点击事件解决方案
不会响应我们自己的点击事件,但是会响应系统的自带的“删除”按钮事件和“备注”事件,那么我们何尝不直接用系统自带的呢,当“确认删除”Label显示出来的时候,点击“备注”也实现“删除”操作,完整源码如下:
- (nullable UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath API_AVAILABLE(ios(11.0)){
UIContextualAction *deleteRowAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:[NSString stringWithFormat:@"删除"] handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
if (self.sureDeleteLabel.superview) { // 说明确认删除Label显示在界面上
NSLog(@"确认删除");
} else {
NSLog(@"显示确认删除Label");
// 核心代码
UIView *rootView = nil; // 这个根view指的是UISwipeActionPullView,最上层的父view
if ([sourceView isKindOfClass:[UILabel class]]) {
rootView = sourceView.superview.superview;
self.sureDeleteLabel.font = ((UILabel *)sourceView).font;
}
self.sureDeleteLabel.frame = CGRectMake(sourceView.bounds.size.width, 0, sourceView.bounds.size.width, sourceView.bounds.size.height);
[sourceView.superview.superview addSubview:self.sureDeleteLabel];
[UIView animateWithDuration:0.7 delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:1 options:UIViewAnimationOptionCurveEaseInOut animations:^{
CGRect labelFrame = self.sureDeleteLabel.frame;
labelFrame.origin.x = 0;
labelFrame.size.width = rootView.bounds.size.width;
self.sureDeleteLabel.frame = labelFrame;
} completion:^(BOOL finished) {
}];
}
}];
UIContextualAction *remarkAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleNormal title:@"备注" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
// 如果确认删除Label显示在界面上,那么本次点击备注的区域响应确认删除按钮事件
if(self.sureDeleteLabel.superview) {
NSLog(@"确认删除");
} else {
NSLog(@"备注");
}
}];
UISwipeActionsConfiguration *config = [UISwipeActionsConfiguration configurationWithActions:@[deleteRowAction,remarkAction]];
config.performsFirstActionWithFullSwipe = NO;
return config;
}
最终效果图:
其余细节
- 如何改变左滑动删除按钮的文字颜色和字体大小?
系统并没有为我们提供改变文字颜色和字体大小的属性,没办法,我们只能获取控件达到我们的目的,那么我们在哪里获取这个控件呢?tableView有一个代理方法:- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
,我们的手指将要左滑时,就会触发这个代理方法,只要在这个代理方法遍历tableView子控件就能拿到左滑动按钮,源码如下:
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(@"将要开始编辑cell");
for (UIView *subView in tableView.subviews) {
if ([subView isKindOfClass:NSClassFromString(@"UISwipeActionPullView")]) {
for (UIView *childView in subView.subviews) {
if ([childView isKindOfClass:NSClassFromString(@"UISwipeActionStandardButton")]) {
UIButton *button = (UIButton *)childView;
button.titleLabel.font = [UIFont systemFontOfSize:18];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
}
}
}
}
}
改变后的效果图:
-
如何去除滑动手势满屏时第一个action变长效果
大家有没有发现,当你的手指左滑cell,使劲往左滑动后,最右边的按钮(称为第一个按钮)会变长,并且送手后直接回调action的 block,想要去除这个效果很简单,只需要设置UISwipeActionsConfiguration
的属性performsFirstActionWithFullSwipe为NO即可,如图: -
如何实现当左滑按钮已经出现时,再次左滑则移除自己添加的“确认删除”Label
这里我并没有找到非常棒的方案,但是也实现了,我是获取tableView的左滑手势,然后给该手势再添加一个方法,如:
// 获取系统左滑手势
for (UIGestureRecognizer *ges in self.tableView.gestureRecognizers) {
if ([ges isKindOfClass:NSClassFromString(@"_UISwipeActionPanGestureRecognizer")]) {
[ges addTarget:self action:@selector(_swipeRecognizerDidRecognize:)];
}
}
// 当左滑按钮已经出现时,再次左滑则移除“确认删除”控件
- (void)_swipeRecognizerDidRecognize:(UISwipeGestureRecognizer *)swip {
if (_sureDeleteLabel.superview) {
[_sureDeleteLabel removeFromSuperview];
_sureDeleteLabel = nil;
}
}
- 如何去除左滑后再使劲右滑的反弹效果
我们发现系统自带的,左滑后,再紧接着使劲右滑,会有反弹效果,微信是没有的,我的解决办法是再上面的那个手势方法里强制将cell的x值改为0,这个方案个人觉得不是很好,但是目前我只知道这种解决方案,如果你有更好的办法,可以给我留言, 实现如下:
- (void)_swipeRecognizerDidRecognize:(UISwipeGestureRecognizer *)swip {
if (_sureDeleteLabel.superview) {
[_sureDeleteLabel removeFromSuperview];
_sureDeleteLabel = nil;
}
CGPoint currentPoint = [swip locationInView:self.tableView];
for (UITableViewCell *cell in self.tableView.visibleCells) {
if (CGRectContainsPoint(cell.frame, currentPoint)) {
if (cell.frame.origin.x > 0) {
cell.frame = CGRectMake(0, cell.frame.origin.y,cell.bounds.size.width, cell.bounds.size.height);
}
}
}
}