仿微信左滑删除

Demo地址:https://github.com/SPStore/WeChatDelete

开门见山,先上微信原生效果图


未命名.gif

这个效果也只有从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;
}

好的,我们已经实现了左滑出现两个按钮,一个是备注,一个是删除,如图:


31FA7CA81C0FE5F36441855AEC5790A1.jpg

我们渲染一下层级结构图,发现iOS11之后层级如下:

31837F3E48B9D65C4BE06F230AE96145.jpg

截图中红方框款起来的就是左滑按钮的层级结构,发现有一个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;
}

最终效果图:


未命名.gif

其余细节

  • 如何改变左滑动删除按钮的文字颜色和字体大小?
    系统并没有为我们提供改变文字颜色和字体大小的属性,没办法,我们只能获取控件达到我们的目的,那么我们在哪里获取这个控件呢?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];
                }
            }
        }
    }
}

改变后的效果图:


397F27E7F540E283EA3680A62051C965.jpg
  • 如何去除滑动手势满屏时第一个action变长效果
    大家有没有发现,当你的手指左滑cell,使劲往左滑动后,最右边的按钮(称为第一个按钮)会变长,并且送手后直接回调action的 block,想要去除这个效果很简单,只需要设置UISwipeActionsConfiguration的属性performsFirstActionWithFullSwipe为NO即可,如图:

    BD977FFFE08279AB4DFDBA7D5DD007CA.jpg

  • 如何实现当左滑按钮已经出现时,再次左滑则移除自己添加的“确认删除”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);
            }
        }
    }
}

Demo地址:https://github.com/SPStore/WeChatDelete

推荐阅读更多精彩内容