作者: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(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可以自取
https://github.com/kiritoSong/KSSideslipCellDemo
朋友會在“發現-看一看”看到你“在看”的內容