iOS--一個高仿微信左滑確認刪除的輪子

前言

一個需求,要求左滑點擊刪除後出現二次確認。和微信一樣。

調研結果如下:

  • iOS11之後,可以通過對系統方法進行改造的方式實現。可以看這篇https://www.jianshu.com/p/aa6ff5d9f965

  • iOS11之前,系統在點擊刪除按鈕之後會自動對擴展按鈕進行回收。無法進行那樣的改造。

於是決定自己寫一個

最初參考了一個16年仿微信左滑的博客https://www.jianshu.com/p/dc57e633de51

由於16年的微信與現在的交互差異太大,所以進行了大量改造,只保留了其對於側滑菜單的創建以及滑動判定的邏輯基礎。

對其中的bug以及功能實現方式進行優化調整,基本實現了現在微信的左滑邏輯功能。


實際效果

伸手黨福利,先看效果不滿意直接右上角就好了。

由於我很懶...所以demo的主體結構基本沒改,側滑菜單創建的邏輯沒做太多修改。

Demo在文章最後


具體到主要的代碼上

我連demo的文件名都懶得改(當然Cell的名字我改了,畢竟我做了三天才做完),就更別提界面了...
下面是一些我修改了的地方,如果你想了解的點在我這找不到。可以試着查看原作者的文章https://www.jianshu.com/p/dc57e633de51

  • 新增了一個專門的側滑容器View

原Demo就是一個VIew,上面循環的創建按鈕使用。
由於新版微信需要很多複雜的交互效果(形變,反彈,確認刪除等等)
我新建了一個KSSideslipContainerView的容器View。
可以很方便的進行二次操作

  • 滾動時收起側滑菜單

原Demo中側滑展示時,是滑動交互式關閉的。

這裏我通過NSProxy對tableView的滑動代理進行攔截

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  
    if (self.target.sideslip) {
        [self.target hiddenAllSideslip];
    }
    
    if ([self.tbDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
        [self.tbDelegate scrollViewWillBeginDragging:scrollView];
    }
    
}
  • 點擊時收起側滑菜單

原Demo中是在cell上添加了一個單擊手勢進行處理

我改爲將didSelectRowAtIndexPath一起放在NSProxy代理中進行攔截了

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (self.target.sideslip) {
        [self.target hiddenAllSideslip];
    }
    
    if ([self.tbDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
        [self.tbDelegate tableView:tableView didSelectRowAtIndexPath:indexPath];
    }
}
  • NSProxy

剛纔說的攔截器

- (void)setTarget:(UITableView *)target {
    _target = target;
    target.sideslipCellProxy = self; //這裏需要讓tableView強引用proxy防止釋放
    self.tbDelegate = target.delegate; //保存tableView原本的delegate,進行轉發
    target.delegate = self; //修改tableView.delegate攔截事件
}

這個東西會在每次側滑容器展示時嘗試綁定與tableVIew進行綁定。當然,它只會綁定一次

- (void)tryBindProxy {
    UITableView * tableView = [self tableView];
    if ([tableView isKindOfClass:[UITableView class]]) {
        if (![tableView.delegate isKindOfClass:[KTSideslipCellProxy class]]) {
            
            //保證一個tableView只會設置一次proxy
            KTSideslipCellProxy *proxy = [KTSideslipCellProxy alloc];
            proxy.target = tableView; //這裏。proxy的target是weak屬性,並不會造成循環引用
        }
    }
}
  • 側滑容器的動畫

原Demo中側滑按鈕並沒有移動,一直是放在cell的最右側

我是通過監聽cell.contentView將側滑容器粘到contentView上。

    if ([keyPath isEqualToString:@"frame"]) {
        
        if (self.btnContainView) {
            KS_setX(self.btnContainView, self.contentView.frame.size.width + self.contentView.frame.origin.x);
        }
    }
}

不過這裏是由於另一個方案有小問題,demo裏我有註釋。大佬們可以研究研究

  • 阻尼效果

原Demo中不允許拖拽超過側滑容器的長度,這和微信不太一樣

if (frame.origin.x + point.x <= -(self.btnContainView.totalWidth)) {
    //超過最大距離,加阻尼
    CGFloat hindrance = (point.x/5);
    if (frame.origin.x + hindrance <= -(self.btnContainView.totalWidth)) {
        frame.origin.x += hindrance;
        cframe.size.width += -hindrance;
        cframe.origin.x += hindrance;
    }else {
        //這裏修復了一個當滑動過快時,導致最初減速時閃動的bug
        frame.origin.x = - self.btnContainView.totalWidth;
        cframe.origin.x = self.contentView.frame.size.width - self.btnContainView.totalWidth;
    }
}else {
    //未到最大距離,正常拖拽
    frame.origin.x += point.x;
    cframe.origin.x += point.x;
}
  • 抽屜效果與過度拉伸的形變

側滑容器以及其上的子View會根據最終寬度,自動調整佈局比例

- (void)scaleToWidth:(CGFloat)width {
    CGFloat needExpandWidth = width - self.totalWidth;
    NSUInteger count = _originSubViews.count;
    CGFloat currentX = 0;
    for (int i = 0; i < count; i++) {
        UIView *s = _originSubViews[i];
        CGRect sframe = s.frame;
        sframe.origin.x = currentX;
        CGFloat sneedExpandWidth = (needExpandWidth * [_originWidths[i] floatValue]/_totalWidth);
        sframe.size.width = [_originWidths[i] floatValue] + sneedExpandWidth;
        s.frame = sframe;
        
        //下一個X起點爲上一個起點+上一個寬度
        currentX += sframe.size.width;
    }
}
  • 確認刪除按鈕的實現

在點擊側滑按鈕的代理事件中,允許傳遞一個View回來。如果傳遞迴了一個View,我會將其放到側滑容器上,並進行佈局的適配。

if ([self.delegate respondsToSelector:@selector(sideslipCell:rowAtIndexPath:didSelectedAtIndex:)]) {
    _nextShowView = [self.delegate sideslipCell:self rowAtIndexPath:self.indexPath didSelectedAtIndex:btn.tag];
    
    /**
        如果有需要繼續展示的View--一般是確認刪除?
        這裏會將其覆蓋到側滑容器上,並且重新以新的View作爲基礎進行佈局
     */
    if (_nextShowView) {
        [_btnContainView addSubview:_nextShowView];
        CGRect frame = CGRectMake(0, 0, _nextShowView.frame.size.width, self.contentView.frame.size.height);

        _nextShowView.frame = CGRectMake(self.btnContainView.originSubViews.lastObject.frame.origin.x, 0, _nextShowView.frame.size.width, self.contentView.frame.size.height);
        _nextShowView.hidden = YES;
        
        [UIView animateWithDuration:0.7 delay:0 usingSpringWithDamping:0.7 initialSpringVelocity:1 options:UIViewAnimationOptionCurveEaseInOut|UIViewAnimationOptionAllowUserInteraction animations:^{
            _nextShowView.frame = frame;
            _btnContainView.frame = frame;
            _nextShowView.hidden = NO;
            [_btnContainView.subButtons setValue:@(YES) forKeyPath:@"hidden"];
            KS_setX(self.contentView, -KS_getW(_nextShowView));
            [self.btnContainView scaleToWidth:_nextShowView.frame.size.width];
        } completion:^(BOOL finished) {
            [_btnContainView.subButtons setValue:@(NO) forKeyPath:@"hidden"];
        }];
    }
}
  • 修改了原Demo內存泄漏的問題

問題出在這

    if (!_tableView) {
        id view = self.superview;
        while (view && [view isKindOfClass:[UITableView class]] == NO) {
            view = [view superview];
        }
        _tableView = (UITableView *)view;
        _tableViewPan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(tableViewPan:)];
        _tableViewPan.delegate = self;
        [_tableView addGestureRecognizer:_tableViewPan];
    }
    return _tableView;
}

修改後

- (UITableView *)tableView {
    id view = self.superview;
    while (view && [view isKindOfClass:[UITableView class]] == NO) {
        view = [view superview];
    }
    if ([view isKindOfClass:[UITableView class]]) {
        return view;
    }else {
        return nil;
    }
}

最後

這個需求整整搞了我三天,還是在修改別人Demo的基礎上,沒成想這麼複雜...
不過好在總算是弄完了

Demo可以自取

當然,如果能點個贊或者給個star我也算沒白忙活

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章