一、需求
之前遇到一個(gè)需求是,要求在scrollview在上下滑動(dòng)時(shí),scrollview顯示區(qū)域高度變化气笙。向上滑動(dòng)時(shí)——拉高次企,向下滑動(dòng)時(shí)——恢復(fù)怯晕。
二、項(xiàng)目中的實(shí)現(xiàn)
由于項(xiàng)目中要實(shí)現(xiàn)的幾個(gè)頁(yè)面都用到了自定義的SITableView缸棵,剛好就在自定義的SITableView中實(shí)現(xiàn)了
1.向外傳遞滑動(dòng)
有以下兩種方案
- 1)協(xié)議 如果是多級(jí)或者是跨層的舟茶,不好要拿到響應(yīng)者,同時(shí)如果視圖層級(jí)改變的話堵第,也需要改變賦值響應(yīng)者的代碼吧凉。可以精準(zhǔn)的傳遞事件給需要改變的視圖踏志,也可以自定義滑動(dòng)距離阀捅,雖然實(shí)際用處不大。本次實(shí)現(xiàn)用的是協(xié)議针余。
還有一種思路是饲鄙,定義一個(gè)BOOL值,標(biāo)識(shí)是否開(kāi)啟滑動(dòng)改變傳遞圆雁,然后向上查找第一個(gè)能響應(yīng)協(xié)議的responder忍级,把它記錄為委托者。
- 2)通知
傳遞數(shù)據(jù)方便伪朽,但不能自定義滑動(dòng)距離轴咱。并且如果多個(gè)界面都注冊(cè)了的話,接受到通知要進(jìn)行判斷烈涮,判斷要調(diào)整大小的視圖是不是在屏幕上朴肺。如果頁(yè)面復(fù)用過(guò)程中,導(dǎo)致某個(gè)視圖加載完成后坚洽,視圖層級(jí)中有父視圖和子視圖都能響應(yīng)通知戈稿,會(huì)出現(xiàn)問(wèn)題,雖然出現(xiàn)的可能性不大酪术。
協(xié)議的代碼如下:
@class SITableView;
@protocol SITableViewUpDownScrollProtocol <NSObject>
//告訴外部對(duì)象器瘪,是向上還是向下滑動(dòng)
- (void)tableView:(SITableView *)tableView updownScroll:(BOOL)isUp;
@optional
// 是否要自定義判斷移動(dòng)的距離
- (CGFloat)tableViewMinMoveDistance:(SITableView *)tableView;
@end
滑動(dòng)方向是向上還是向下翠储,應(yīng)該用枚舉的,偷懶了
2.SITableView中的主要變動(dòng)
在scrollViewDidScroll :
方法中橡疼,判斷contentOffset.y的變化援所,與前一刻的差值作為上下的依據(jù)。
要考慮以下幾個(gè)問(wèn)題:
1.只有當(dāng)用戶手動(dòng)滑動(dòng)時(shí)欣除,才改變視圖高度住拭。需要記錄是不是手動(dòng)拖拽,雖然历帚,scrollview有dragging滔岳,但不夠精確,在手松開(kāi)減速時(shí)依然是YES挽牢,不符合要求
2.需要記錄初始值谱煤,來(lái)做參考
3.要移動(dòng)一定距離,才能判斷是否執(zhí)行回調(diào)禽拔,避免有時(shí)手觸碰屏幕引起的誤操作
4.攔截的方法刘离,不能影響原方法的調(diào)用
- 1.增加私有屬性,協(xié)助判斷
//是不是手動(dòng)移動(dòng)
@property (nonatomic, assign, getter=isManuallyMoving) BOOL manuallyMoving;
//開(kāi)始手動(dòng)移動(dòng)時(shí)contentOffset.y值
@property (nonatomic, assign) CGFloat startOffsetY;
//tableview的新的delegate睹栖,用來(lái)判斷是否要攔截
@property (nonatomic, strong) SITableViewWeakProxy *weakProxy;
//默認(rèn)最小移動(dòng)距離 5
@property (nonatomic, assign) CGFloat minMoveDistance;
- 2.實(shí)現(xiàn)
#pragma mark - 上下滑動(dòng)回調(diào)
//調(diào)用有參無(wú)返回值的方法
- (void)callTableViewUpDownScrollProtocol:(BOOL)isUp {
if (self.upDownScrollDelegate == nil) {
return;
}
// 1. 根據(jù)方法創(chuàng)建簽名對(duì)象sig
NSMethodSignature *sig = [self.upDownScrollDelegate methodSignatureForSelector:@selector(tableView:updownScroll:)];
// 2. 根據(jù)簽名對(duì)象創(chuàng)建調(diào)用對(duì)象invocation
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
// 3. 設(shè)置調(diào)用對(duì)象的相關(guān)信息
invocation.target = self.upDownScrollDelegate;
invocation.selector = @selector(tableView:updownScroll:);
SITableView *tempSelf = self;
// 參數(shù)必須從第2個(gè)索引開(kāi)始硫惕,因?yàn)榍皟蓚€(gè)已經(jīng)被target和selector使用
[invocation setArgument:&tempSelf atIndex:2];
[invocation setArgument:&isUp atIndex:3];
// 4. 調(diào)用方法
[invocation invoke];
}
#pragma mark - 攔截的協(xié)議方法
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
self.manuallyMoving = NO;
//不影響原有的邏輯,回調(diào)原來(lái)delegate的方法
if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) {
[self.weakProxy.originTarget scrollViewDidEndDragging:scrollView willDecelerate:decelerate];
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.manuallyMoving = YES;
self.startOffsetY = scrollView.contentOffset.y;
//不影響原有的邏輯野来,回調(diào)原來(lái)delegate的方法
if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewWillBeginDragging:)]) {
[self.weakProxy.originTarget scrollViewWillBeginDragging:scrollView];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (self.isManuallyMoving) {
if (self.startOffsetY < scrollView.contentOffset.y - self.minMoveDistance) {
[self callTableViewUpDownScrollProtocol:YES];
}
if (self.startOffsetY > scrollView.contentOffset.y + self.minMoveDistance) {
[self callTableViewUpDownScrollProtocol:NO];
}
}
self.startOffsetY = scrollView.contentOffset.y;
//不影響原有的邏輯恼除,回調(diào)原來(lái)delegate的方法
if ([self.weakProxy.originTarget respondsToSelector:@selector(scrollViewDidScroll:)]) {
[self.weakProxy.originTarget scrollViewDidScroll:scrollView];
}
}
#pragma mark - setter與getter
- (void)setDelegate:(id<UITableViewDelegate>)delegate {
self.weakProxy.originTarget = delegate;
[super setDelegate:self.weakProxy];
}
- (void)setUpDownScrollDelegate:(id<SITableViewUpDownScrollProtocol>)upDownScrollDelegate {
if (upDownScrollDelegate && [upDownScrollDelegate conformsToProtocol:@protocol(SITableViewUpDownScrollProtocol)] && [upDownScrollDelegate respondsToSelector:@selector(tableView:updownScroll:)]) {
_upDownScrollDelegate = upDownScrollDelegate;
if ([upDownScrollDelegate respondsToSelector:@selector(tableViewMinMoveDistance:)]) {
self.minMoveDistance = [upDownScrollDelegate tableViewMinMoveDistance:self];
}
}
if (upDownScrollDelegate == nil) {
_upDownScrollDelegate = upDownScrollDelegate;
}
}
- (SITableViewWeakProxy *)weakProxy {
if (_weakProxy == nil) {
_weakProxy = [SITableViewWeakProxy alloc];
_weakProxy.interceptionTarget = self;
}
return _weakProxy;
}
注意 [SITableViewWeakProxy alloc];
這樣寫(xiě)沒(méi)有錯(cuò),它沒(méi)有init方法曼氛。
3.SITableViewWeakProxy的實(shí)現(xiàn)
為什么要做的這樣復(fù)雜豁辉,
不直接把delegate設(shè)為自己,用一個(gè)屬性記錄原始的delegate呢搪锣?如果這樣做了秋忙,tableview的UITableViewDelegate協(xié)議中的其他方法呢,怎么把協(xié)議中的方法傳遞給原始的delegate呢构舟。實(shí)現(xiàn)所有的方法灰追,在里面判斷原始的delegate是否實(shí)現(xiàn)了,原始未實(shí)現(xiàn)的但方法需要返回值的你怎么操作狗超。如果里面后面新增了方法怎么辦弹澎,一個(gè)個(gè)版本維護(hù)更新?
走消息轉(zhuǎn)發(fā)努咐,UITableViewDelegate協(xié)議中的很多方法是optional苦蒿,會(huì)調(diào)用respondsToSelector來(lái)判斷是否協(xié)議中某個(gè)方法,這個(gè)地方的響應(yīng)者是SITableView的實(shí)例渗稍,它明顯沒(méi)有實(shí)現(xiàn)協(xié)議中的其他方法佩迟,就無(wú)法調(diào)用了团滥。當(dāng)然也可以重寫(xiě)respondsToSelector,但怎么判斷這個(gè)sel是UITableViewDelegate協(xié)議中的方法报强,一個(gè)個(gè)列出來(lái)
使用SITableViewWeakProxy灸姊,是實(shí)例不會(huì)在方法列表中查找,而是直接走消息轉(zhuǎn)發(fā)秉溉,效率高力惯,也安全,不用擔(dān)心其他的影響召嘶。包括respondsToSelector方法也是走的消息轉(zhuǎn)發(fā)父晶,所以在具體的實(shí)現(xiàn)中,要特殊處理弄跌,判斷這個(gè)方法的參數(shù)甲喝,如果是要攔截的三個(gè)方法,就要攔截碟绑。
@interface SITableViewWeakProxy : NSProxy <UITableViewDelegate>
@property (nonatomic, weak) NSObject<UITableViewDelegate> *originTarget;
@property (nonatomic, weak) NSObject *interceptionTarget;
@end
@implementation SITableViewWeakProxy
//- (id)forwardingTargetForSelector:(SEL)selector {
// NSLog(@"%@...%@", self, NSStringFromSelector(selector));
// for (NSString *interceptionSEL in self.interceptionSELS) {
// if (NSSelectorFromString(interceptionSEL) == selector) {
// return _interceptionTarget;
// }
// }
// return _originTarget;
//}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.originTarget methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
//這個(gè)很重要俺猿,SITableViewWeakProxy不能響應(yīng)respondsToSelector方法茎匠,只是做轉(zhuǎn)發(fā)格仲,所以需要特殊判斷下
if (self.interceptionTarget && invocation.selector == @selector(respondsToSelector:)) {
SEL parameterSel;
[invocation getArgument:¶meterSel atIndex:2];
if ([self interceptionSelector:parameterSel]) {
[invocation invokeWithTarget:self.interceptionTarget];
return;
}
}else if (self.interceptionTarget && [self interceptionSelector:invocation.selector]) {
[invocation invokeWithTarget:self.interceptionTarget];
return;
}
//不需要攔截,直接調(diào)用原來(lái)的delegate
[invocation invokeWithTarget:self.originTarget];
}
//只需要攔截這三個(gè)方法诵冒,不需其他方法
- (BOOL)interceptionSelector:(SEL)sel {
return sel == @selector(scrollViewDidScroll:) || sel == @selector(scrollViewDidEndDragging:willDecelerate:) || sel == @selector(scrollViewWillBeginDragging:);
}
@end
三凯肋、scrollview分類(lèi)的實(shí)現(xiàn)
ps:以下來(lái)自5月1日補(bǔ)充
@selector(setDelegate:)
@selector(delegate)
是一個(gè)屬性的set與get方法,它們是一個(gè)整體汽馋,不能拆分開(kāi)來(lái)侮东,需要都hook,之前思慮不周全豹芯,沒(méi)考慮到這一點(diǎn)悄雅。比如說(shuō),不斷的調(diào)用get方法然后再重新賦值給set方法铁蹈,之前的實(shí)現(xiàn)就會(huì)有問(wèn)題宽闲,改變了原有的實(shí)現(xiàn),雖然一般不會(huì)這么做握牧,但程序要嚴(yán)謹(jǐn)容诬,不留漏洞。
分類(lèi)方式的實(shí)現(xiàn)沒(méi)有采用協(xié)議的方式沿腰,主要是考慮到幾點(diǎn):
如果有協(xié)議回調(diào)览徒、又有通知可以選,那么在開(kāi)啟監(jiān)聽(tīng)方法設(shè)計(jì)不夠優(yōu)雅
這樣在組件化使用中更加方便颂龙,耦合性比協(xié)議小
不在實(shí)現(xiàn)中統(tǒng)一判斷最小滑動(dòng)距離习蓬,而是直接傳遞纽什,由使用者自行判斷,靈活性更大躲叼;之前的最小滑動(dòng)距離設(shè)定不好操作也是一方面
實(shí)現(xiàn)方案說(shuō)明:
通知的userInfo中稿湿,有兩個(gè)key,一直是滑動(dòng)的距離(當(dāng)前位置減去上一次的位置)押赊,還有一個(gè)就是哪一個(gè)scrollView滑動(dòng)發(fā)出的通知饺藤,來(lái)解決使用通知引起的多點(diǎn)觸發(fā),不知道該不該響應(yīng)的問(wèn)題流礁。
消息轉(zhuǎn)發(fā)者與攔截方法判斷分別在兩個(gè)類(lèi)實(shí)現(xiàn)涕俗,雖然職責(zé)分開(kāi)了,但是之間互相耦合神帅,沒(méi)有通過(guò)接口(協(xié)議)編程再姑。消息轉(zhuǎn)發(fā)類(lèi)的實(shí)現(xiàn)參考了YYKit里面的實(shí)現(xiàn)。
-
兩種實(shí)現(xiàn)方式找御,實(shí)際上大同小異
通過(guò)函數(shù)指針的方式元镀,hook方法的實(shí)現(xiàn)。這里替換的是
UIScrollView
這個(gè)類(lèi)的delegate
屬性對(duì)應(yīng)的兩個(gè)方法霎桅,使用GCD確保只會(huì)進(jìn)行一次通過(guò)派生一個(gè)子類(lèi)栖疑,類(lèi)似KVO模式。調(diào)用方法使用的是編譯后的方法
objc_msgSendSuper
滔驶,還要處理如果之前這個(gè)類(lèi)添加過(guò)KVO的情況遇革,并且處理的用的是KVC,如果有變動(dòng)揭糕,不會(huì)知道萝快。如果有其他類(lèi)也使用這種方案,將互相沖突抵消掉著角。思路與實(shí)現(xiàn)參考了IMYAOPTableView測(cè)試中分了兩種情況:在開(kāi)啟監(jiān)聽(tīng)之前delegate有值揪漩;開(kāi)啟監(jiān)聽(tīng)之后才設(shè)置delegate。通過(guò)宏來(lái)進(jìn)行不同情況測(cè)試吏口。兩種實(shí)現(xiàn)方式也是通過(guò)宏來(lái)控制切換奄容。
具體代碼實(shí)現(xiàn)參見(jiàn):WeakProxy
對(duì)于參考與借鑒的源碼在這里一并表示感謝!歡迎斧正锨侯!