歡迎光臨
每天分享高質量文章

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

作者:kirito_song
連結:https://www.jianshu.com/p/a08b6db47014

前言

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

 

 

調研結果如下:

 

  • 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         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(00, _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可以自取

https://github.com/kiritoSong/KSSideslipCellDemo

 

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

已同步到看一看
贊(0)

分享創造快樂