美文网首页代码片段
仿微信左滑删除

仿微信左滑删除

作者: S型身材的猪 | 来源:发表于2018-12-10 12:30 被阅读291次

    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

    相关文章

      网友评论

        本文标题:仿微信左滑删除

        本文链接:https://www.haomeiwen.com/subject/lqcxhqtx.html