前言
一個(gè)需求责蝠,要求左滑點(diǎn)擊刪除后出現(xiàn)二次確認(rèn)喷户。和微信一樣毡泻。
調(diào)研結(jié)果如下:
iOS11之后,可以通過(guò)對(duì)系統(tǒng)方法進(jìn)行改造的方式實(shí)現(xiàn)室叉。可以看這篇http://www.reibang.com/p/aa6ff5d9f965
iOS11之前硫惕,系統(tǒng)在點(diǎn)擊刪除按鈕之后會(huì)自動(dòng)對(duì)擴(kuò)展按鈕進(jìn)行回收茧痕。無(wú)法進(jìn)行那樣的改造。
于是決定自己寫(xiě)一個(gè)
最初參考了一個(gè)16年仿微信左滑的博客http://www.reibang.com/p/dc57e633de51
由于16年的微信與現(xiàn)在的交互差異太大恼除,所以進(jìn)行了大量改造踪旷,只保留了其對(duì)于側(cè)滑菜單的創(chuàng)建以及滑動(dòng)判定的邏輯基礎(chǔ)。
對(duì)其中的bug以及功能實(shí)現(xiàn)方式進(jìn)行優(yōu)化調(diào)整缚柳,基本實(shí)現(xiàn)了現(xiàn)在微信的左滑邏輯功能埃脏。
實(shí)際效果
伸手黨福利,先看效果不滿意直接右上角就好了秋忙。
由于我很懶...所以demo的主體結(jié)構(gòu)基本沒(méi)改彩掐,側(cè)滑菜單創(chuàng)建的邏輯沒(méi)做太多修改。
Demo在文章最后
具體到主要的代碼上
我連demo的文件名都懶得改(當(dāng)然Cell的名字我改了灰追,畢竟我做了三天才做完)堵幽,就更別提界面了...
下面是一些我修改了的地方狗超,如果你想了解的點(diǎn)在我這找不到∑酉拢可以試著查看原作者的文章http://www.reibang.com/p/dc57e633de51
-
新增了一個(gè)專門(mén)的側(cè)滑容器View
原Demo就是一個(gè)VIew努咐,上面循環(huán)的創(chuàng)建按鈕使用。
由于新版微信需要很多復(fù)雜的交互效果(形變,反彈,確認(rèn)刪除等等)
我新建了一個(gè)KSSideslipContainerView
的容器View殴胧。
可以很方便的進(jìn)行二次操作
-
滾動(dòng)時(shí)收起側(cè)滑菜單
原Demo中側(cè)滑展示時(shí)渗稍,是滑動(dòng)交互式關(guān)閉的。
這里我通過(guò)NSProxy對(duì)tableView的滑動(dòng)代理進(jìn)行攔截
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
if (self.target.sideslip) {
[self.target hiddenAllSideslip];
}
if ([self.tbDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
[self.tbDelegate scrollViewWillBeginDragging:scrollView];
}
}
-
點(diǎn)擊時(shí)收起側(cè)滑菜單
原Demo中是在cell上添加了一個(gè)單擊手勢(shì)進(jìn)行處理
我改為將didSelectRowAtIndexPath
一起放在NSProxy代理中進(jìn)行攔截了
- (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
剛才說(shuō)的攔截器
- (void)setTarget:(UITableView *)target {
_target = target;
target.sideslipCellProxy = self; //這里需要讓tableView強(qiáng)引用proxy防止釋放
self.tbDelegate = target.delegate; //保存tableView原本的delegate团滥,進(jìn)行轉(zhuǎn)發(fā)
self.tbDataSource = target.dataSource;//保存tableView原本的dataSource竿屹,進(jìn)行轉(zhuǎn)發(fā)
target.delegate = self; //修改tableView.delegate攔截事件
}
這個(gè)東西會(huì)在每次側(cè)滑容器展示時(shí)嘗試綁定與tableVIew進(jìn)行綁定。當(dāng)然灸姊,它只會(huì)綁定一次
- (void)tryBindProxy {
UITableView * tableView = [self tableView];
if ([tableView isKindOfClass:[UITableView class]]) {
if (![tableView.delegate isKindOfClass:[KTSideslipCellProxy class]]) {
//保證一個(gè)tableView只會(huì)設(shè)置一次proxy
KTSideslipCellProxy *proxy = [KTSideslipCellProxy alloc];
proxy.target = tableView; //這里拱燃。proxy的target是weak屬性,并不會(huì)造成循環(huán)引用
}
}
}
之后力惯,利用NSProxy的特點(diǎn)碗誉,將未攔截的消息轉(zhuǎn)發(fā)給原本的代理者:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
id res;
if ([self.tbDelegate respondsToSelector:aSelector]) {
res = self.tbDelegate;
}else if ([self.tbDataSource respondsToSelector:aSelector]) {
res = self.tbDataSource;
}
return res;
}
-
側(cè)滑容器的動(dòng)畫(huà)
原Demo中側(cè)滑按鈕并沒(méi)有移動(dòng),一直是放在cell的最右側(cè)
我是通過(guò)監(jiān)聽(tīng)cell.contentView將側(cè)滑容器粘到contentView上父晶。
if ([keyPath isEqualToString:@"frame"]) {
if (self.btnContainView) {
KS_setX(self.btnContainView, self.contentView.frame.size.width + self.contentView.frame.origin.x);
}
}
}
不過(guò)這里是由于另一個(gè)方案有小問(wèn)題哮缺,demo里我有注釋。大佬們可以研究研究
-
阻尼效果
原Demo中不允許拖拽超過(guò)側(cè)滑容器的長(zhǎng)度诱建,這和微信不太一樣
if (frame.origin.x + point.x <= -(self.btnContainView.totalWidth)) {
//超過(guò)最大距離蝴蜓,加阻尼
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 {
//這里修復(fù)了一個(gè)當(dāng)滑動(dòng)過(guò)快時(shí),導(dǎo)致最初減速時(shí)閃動(dòng)的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;
}
-
抽屜效果與過(guò)度拉伸的形變
側(cè)滑容器以及其上的子View會(huì)根據(jù)最終寬度茎匠,自動(dòng)調(diào)整布局比例
- (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;
//下一個(gè)X起點(diǎn)為上一個(gè)起點(diǎn)+上一個(gè)寬度
currentX += sframe.size.width;
}
}
-
確認(rèn)刪除按鈕的實(shí)現(xiàn)
在點(diǎn)擊側(cè)滑按鈕的代理事件中,允許傳遞一個(gè)View回來(lái)押袍。如果傳遞回了一個(gè)View诵冒,我會(huì)將其放到側(cè)滑容器上,并進(jìn)行布局的適配谊惭。
if ([self.delegate respondsToSelector:@selector(sideslipCell:rowAtIndexPath:didSelectedAtIndex:)]) {
_nextShowView = [self.delegate sideslipCell:self rowAtIndexPath:self.indexPath didSelectedAtIndex:btn.tag];
/**
如果有需要繼續(xù)展示的View--一般是確認(rèn)刪除?
這里會(huì)將其覆蓋到側(cè)滑容器上汽馋,并且重新以新的View作為基礎(chǔ)進(jìn)行布局
*/
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內(nèi)存泄漏的問(wèn)題
問(wèn)題出在這
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;
}
}
最后
這個(gè)需求整整搞了我三天,還是在修改別人Demo的基礎(chǔ)上圈盔,沒(méi)成想這么復(fù)雜...
不過(guò)好在總算是弄完了
Demo可以自取
當(dāng)然豹芯,如果能點(diǎn)個(gè)贊或者給個(gè)star我也算沒(méi)白忙活