scrollview在上下滑动时,改变视图高度

一、需求

之前遇到一个需求是,要求在scrollview在上下滑动时,scrollview显示区域高度变化。向上滑动时——拉高,向下滑动时——恢复。

二、项目中的实现

由于项目中要实现的几个页面都用到了自定义的SITableView,刚好就在自定义的SITableView中实现了

1.向外传递滑动

有以下两种方案

  • 1)协议 如果是多级或者是跨层的,不好要拿到响应者,同时如果视图层级改变的话,也需要改变赋值响应者的代码。可以精准的传递事件给需要改变的视图,也可以自定义滑动距离,虽然实际用处不大。本次实现用的是协议。

还有一种思路是,定义一个BOOL值,标识是否开启滑动改变传递,然后向上查找第一个能响应协议的responder,把它记录为委托者。

  • 2)通知
    传递数据方便,但不能自定义滑动距离。并且如果多个界面都注册了的话,接受到通知要进行判断,判断要调整大小的视图是不是在屏幕上。如果页面复用过程中,导致某个视图加载完成后,视图层级中有父视图和子视图都能响应通知,会出现问题,虽然出现的可能性不大。

协议的代码如下:

@class SITableView;
@protocol SITableViewUpDownScrollProtocol <NSObject>
//告诉外部对象,是向上还是向下滑动
- (void)tableView:(SITableView *)tableView updownScroll:(BOOL)isUp;
@optional
// 是否要自定义判断移动的距离
- (CGFloat)tableViewMinMoveDistance:(SITableView *)tableView;

@end

滑动方向是向上还是向下,应该用枚举的,偷懒了

2.SITableView中的主要变动

scrollViewDidScroll :方法中,判断contentOffset.y的变化,与前一刻的差值作为上下的依据。
要考虑以下几个问题:

1.只有当用户手动滑动时,才改变视图高度。需要记录是不是手动拖拽,虽然,scrollview有dragging,但不够精确,在手松开减速时依然是YES,不符合要求
2.需要记录初始值,来做参考
3.要移动一定距离,才能判断是否执行回调,避免有时手触碰屏幕引起的误操作
4.拦截的方法,不能影响原方法的调用

  • 1.增加私有属性,协助判断
//是不是手动移动
@property (nonatomic, assign, getter=isManuallyMoving) BOOL manuallyMoving;
//开始手动移动时contentOffset.y值
@property (nonatomic, assign) CGFloat startOffsetY;
//tableview的新的delegate,用来判断是否要拦截
@property (nonatomic, strong) SITableViewWeakProxy *weakProxy;
//默认最小移动距离 5
@property (nonatomic, assign) CGFloat minMoveDistance;
  • 2.实现
#pragma mark - 上下滑动回调
//调用有参无返回值的方法
- (void)callTableViewUpDownScrollProtocol:(BOOL)isUp {
    
    if (self.upDownScrollDelegate == nil) {
        return;
    }
    // 1. 根据方法创建签名对象sig
    NSMethodSignature *sig = [self.upDownScrollDelegate methodSignatureForSelector:@selector(tableView:updownScroll:)];
    
    // 2. 根据签名对象创建调用对象invocation
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    
    // 3. 设置调用对象的相关信息
    invocation.target = self.upDownScrollDelegate;
    invocation.selector = @selector(tableView:updownScroll:);
 
    SITableView *tempSelf = self;
    // 参数必须从第2个索引开始,因为前两个已经被target和selector使用
    [invocation setArgument:&tempSelf atIndex:2];
    [invocation setArgument:&isUp atIndex:3];
    
    // 4. 调用方法
    [invocation invoke];
    
}
#pragma mark - 拦截的协议方法

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    self.manuallyMoving = NO;
    //不影响原有的逻辑,回调原来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;

    //不影响原有的逻辑,回调原来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;
    
    //不影响原有的逻辑,回调原来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];这样写没有错,它没有init方法。

3.SITableViewWeakProxy的实现

为什么要做的这样复杂,
不直接把delegate设为自己,用一个属性记录原始的delegate呢?如果这样做了,tableview的UITableViewDelegate协议中的其他方法呢,怎么把协议中的方法传递给原始的delegate呢。实现所有的方法,在里面判断原始的delegate是否实现了,原始未实现的但方法需要返回值的你怎么操作。如果里面后面新增了方法怎么办,一个个版本维护更新?
走消息转发,UITableViewDelegate协议中的很多方法是optional,会调用respondsToSelector来判断是否协议中某个方法,这个地方的响应者是SITableView的实例,它明显没有实现协议中的其他方法,就无法调用了。当然也可以重写respondsToSelector,但怎么判断这个sel是UITableViewDelegate协议中的方法,一个个列出来

使用SITableViewWeakProxy,是实例不会在方法列表中查找,而是直接走消息转发,效率高,也安全,不用担心其他的影响。包括respondsToSelector方法也是走的消息转发,所以在具体的实现中,要特殊处理,判断这个方法的参数,如果是要拦截的三个方法,就要拦截。

@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 {
    //这个很重要,SITableViewWeakProxy不能响应respondsToSelector方法,只是做转发,所以需要特殊判断下
    if (self.interceptionTarget && invocation.selector == @selector(respondsToSelector:)) {
        SEL parameterSel;
        [invocation getArgument:&parameterSel atIndex:2];
        
        if ([self interceptionSelector:parameterSel]) {
            [invocation invokeWithTarget:self.interceptionTarget];
            return;
        }
      
    }else if (self.interceptionTarget && [self interceptionSelector:invocation.selector]) {
        [invocation invokeWithTarget:self.interceptionTarget];
        return;
    }
    //不需要拦截,直接调用原来的delegate
    [invocation invokeWithTarget:self.originTarget];
}
//只需要拦截这三个方法,不需其他方法
- (BOOL)interceptionSelector:(SEL)sel {
    return  sel == @selector(scrollViewDidScroll:) || sel == @selector(scrollViewDidEndDragging:willDecelerate:) || sel == @selector(scrollViewWillBeginDragging:);
}

@end

三、scrollview分类的实现

ps:以下来自5月1日补充

@selector(setDelegate:) @selector(delegate)一个属性的set与get方法,它们是一个整体,不能拆分开来,需要都hook,之前思虑不周全,没考虑到这一点。比如说,不断的调用get方法然后再重新赋值给set方法,之前的实现就会有问题,改变了原有的实现,虽然一般不会这么做,但程序要严谨,不留漏洞。

分类方式的实现没有采用协议的方式,主要是考虑到几点:

  • 如果有协议回调、又有通知可以选,那么在开启监听方法设计不够优雅

  • 这样在组件化使用中更加方便,耦合性比协议小

  • 不在实现中统一判断最小滑动距离,而是直接传递,由使用者自行判断,灵活性更大;之前的最小滑动距离设定不好操作也是一方面

实现方案说明:

  1. 通知的userInfo中,有两个key,一直是滑动的距离(当前位置减去上一次的位置),还有一个就是哪一个scrollView滑动发出的通知,来解决使用通知引起的多点触发,不知道该不该响应的问题。

  2. 消息转发者与拦截方法判断分别在两个类实现,虽然职责分开了,但是之间互相耦合,没有通过接口(协议)编程。消息转发类的实现参考了YYKit里面的实现。

  3. 两种实现方式,实际上大同小异

    • 通过函数指针的方式,hook方法的实现。这里替换的是UIScrollView这个类的delegate属性对应的两个方法,使用GCD确保只会进行一次

    • 通过派生一个子类,类似KVO模式。调用方法使用的是编译后的方法objc_msgSendSuper ,还要处理如果之前这个类添加过KVO的情况,并且处理的用的是KVC,如果有变动,不会知道。如果有其他类也使用这种方案,将互相冲突抵消掉。思路与实现参考了IMYAOPTableView

    • 测试中分了两种情况:在开启监听之前delegate有值;开启监听之后才设置delegate。通过宏来进行不同情况测试。两种实现方式也是通过宏来控制切换。

具体代码实现参见:WeakProxy
对于参考与借鉴的源码在这里一并表示感谢!欢迎斧正!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容