美文网首页程序员
基于 responder chain 的交互

基于 responder chain 的交互

作者: 申申申申申 | 来源:发表于2018-09-20 13:29 被阅读259次
    • 目录
    • 前言
    • 推导
    • 实现
    • 适用场景
    • 栗子
    • 优化
    • 致谢

    1. 前言

    抱歉,最近事情有些烦,文章没有更新

    这几天在着手重构公司产品的首页模块,真的是不想吐槽接手的这份代码,简直就是一坨粑粑,完全没有架构和设计模式可言,有些模块所有的logicviewaction基本全都写在C中,可以看下面截图

    某个C

    当然👆这个图,主要并不是意在代码行数,而是在于这个C保存状态的数量

    这个纯属吐槽,等项目几个主要模块重构完成,单独写一篇对 “天下武功出少林” 的MVC的使用


    2. 推导

    基于MVCV的事件响应需要传递到C中,由C来进行处理(自我更新 或者 更改M),这种

    那么我们先来观察下继承链~

    我们知道VC的父类是UIResponder

    • UIViewController - UIResponder - NSObject

    V中常规展示的来讲,无非就是view上或者cell上有各种btn,各种view,各种label等,排列组合,然后展示给用户,我们看下其各自的继承链

    • UIView - UIResponder - NSObject
    • UITableView - UIScrollView - UIView - UIResponder - NSObject
    • UITableViewCell - UIView - UIResponder - NSObject
    • UIButton - UIControl - UIView - UIResponder - NSObject
    • UILabel - UIView - UIResponder - NSObject
    • ……

    我们会发现,其中有两个比较重要的类,首先是NSObject这个是顶级父类,实力毋庸置疑;其次就是UIResponder这个类和其一个关键属性nextResponder

    #if UIKIT_DEFINE_AS_PROPERTIES
    @property(nonatomic, readonly, nullable) UIResponder *nextResponder;
    #else
    - (nullable UIResponder*)nextResponder;
    #endif
    

    正是这个nextResponder,可以让我们在基于responder chain来寻找我们需要响应的C

    权威解释

    3. 实现

    实现还是超简单~
    仅仅需要一个分类即可~

    • 参数eventName:是当前响应事件的标识(eg,可以用来区分用户是点击了btn还是view),可以在C中用来区分响应来源
    • 参数userInfo:可以用来传递一些参数
    #import <UIKit/UIKit.h>
    @interface UIResponder (FFRouter)
    - (void)ff_routerEventWithEventName:(NSString *)eventName userInfo:(NSDictionary *)userInfo;
    @end
    

    实现1:

    #import "UIResponder+FFRouter.h"
    
    @implementation UIResponder (FFRouter)
    // 空
    - (void)ff_routerEventWithEventName:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
    
    }
    @end
    

    是的,这里是在分类中实现了一个空方法
    因为笔者会基于responder chain直接寻找目标C,在V中调用这个方法,在目标C中,实现即可

    实现2:
    当然也可这样写

    #import "UIResponder+FFRouter.h"
    @implementation UIResponder (FFRouter)
    - (void)ff_routerEventWithEventName:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
       [[self nextResponder] ff_routerEventWithEventName:eventName userInfo:userInfo];
    }
    @end
    

    这种写法使用时候
    V中直接调用

    [self ff_routerEventWithEventName:FF_HomeLinkCellDidClickButton userInfo:@{@"ff_linkcell_buttontag" : [NSString stringWithFormat:@"%ld", (long)button.tag]}]
    

    但是这种写法
    如果需要在当前viewnextresponder中继续向上传递的话
    则需要在当前viewnextresponder中调用

    [super ff_routerEventWithEventName:eventName userInfo:userInfo]
    

    综上,笔者在重构的时候选择的是实现1,其实无论是1还是2,只要直接找到目标responder,其实都没影响,不需要调用super


    4. 适用场景

    我们知道,在VC的交互中,V经常使用protocolblock或其他来将响应传递到C

    此时V可以分为两种情况(以cell代理为例)

    • V的布局简单,使用一次代理即可(如下图,cell中的btn需要传递响应事件到C
    • V的布局复杂(如下图,cell中有自定义的VAVA中有VB,……,最终有一个btn,此时需要将btn的响应事件传递到C,当然可以使用代理一层层的向上传递,直到C,那么就需要写好层多代理(当然,这种情况也可以使用notify,但是并不建议~))

    当然,本文介绍的方案,笔者觉得无论是情况1还是情况2都可以完美解决~


    5. 栗子

    • 首先是👆的分类
    • 定义事件常量
    UIKIT_EXTERN NSString * const FF_HomeBannerCellEventNameButtonClick;
    NSString * const FF_HomeBannerCellEventNameButtonClick = @"ff_button_click";
    
    • 响应btn的时候
    - (void)didClickedButton:(UIButton *)button {
        UIResponder *responder = self.nextResponder;
        while (![responder isKindOfClass:NSClassFromString(@"JHTHomeVC")]) {
            responder = responder.nextResponder;
        }
        if ([responder isKindOfClass:NSClassFromString(@"JHTHomeVC")]) {
            [responder ff_routerEventWithEventName:FF_HomeBannerCellEventNameButtonClick userInfo:@{@"ff_banner_index" : [NSString stringWithFormat:@"%ld", index]}];
        }   
    }
    
    • 在目标C中,通过判断eventname来判断事件来源,userinfo中是传递的参数
    - (void)ff_routerEventWithEventName:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
       // do something
    }
    

    6. 优化

    其实在C中的实现,是有问题的
    如果在view中有多个触发点击的地方,或者C中的tableview中有多个不同类型的cell,每个cell都是使用responder chain来交互的话,那么C中的ff_routerEventWithEventName: userInfo:实现必定存在if...elseif... ...else,作为有洁癖的程序员,真是无法忍受

    因此,使用NSInvocation进行如下优化~

    那么问题又来了,什么是 NSInvocation?
    NSInvocation是一种消息处理机制,具体的看下图

    权威解释
    其实在之前的关于 消息转发 - 完整转发 中有使用到~ 有兴趣的同学可以点进去see see

    继续正文

    • 首先在C中创建一个字典
    @property (nonatomic, strong) NSDictionary<NSString *, NSInvocation *> *invocationDic;
    
    • 懒加载的时候,根据eventname来存储对应的方法实现
    - (NSDictionary<NSString *,NSInvocation *> *)invocationDic {
        if (!_invocationDic) {
            extern NSString * const FF_HomeBannerCellEventNameBannerClick;
            extern NSString * const FF_HomeBannerCellEventNameButtonClick;
            extern NSString * const FF_HomeNoticeCellEventNameClick;
            extern NSString * const FF_HomeBdCellDidSelected;
            extern NSString * const FF_HomeLinkCellDidClickButton;
    
            _invocationDic = @{ FF_HomeBannerCellEventNameBannerClick : [self ff_creatInvocationWithSelector:@selector(ff_actionOfBannerCellOfBannerClick:)],
                               FF_HomeBannerCellEventNameButtonClick : [self ff_creatInvocationWithSelector:@selector(ff_actionOfBannerCellOfButtonClick:)],
                               FF_HomeNoticeCellEventNameClick : [self ff_creatInvocationWithSelector:@selector(ff_actionOfNoticeCellOfClick:)],
                               FF_HomeBdCellDidSelected : [self ff_creatInvocationWithSelector:@selector(ff_actionOfBdCellSelected:)],
                               FF_HomeLinkCellDidClickButton : [self ff_creatInvocationWithSelector:@selector(ff_actionOfLinkCellSelected:)]
                               };
        }
        return _invocationDic;
    }
    
    • ff_creatInvocationWithSelector:的实现(这个方法,笔者是抽离到一个分类中,可供整个项目使用)
    - (NSInvocation *)ff_creatInvocationWithSelector:(SEL)selector {
        NSMethodSignature *signature = [self.class instanceMethodSignatureForSelector:selector];
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
        invocation.target = self;
        invocation.selector = selector;
        return invocation;
    }
    
    • 那么responder chainC调用即可如下
      那么为什么需要把userinfo的引用放在2位置呐?
      因为方法调用的时候,有两个默认参数,0 -> id self1 -> SEL _cmd
      即只可以在2位置插入我们的参数
    - (void)ff_routerEventWithEventName:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
        NSInvocation *invocation = self.invocationDic[eventName];
        [invocation setArgument:&userInfo atIndex:2];
        [invocation invoke];
    }
    

    最后看下重构后的首页吧~

    优化后,没有了if...elseif... ...else,而且使用这种交互,事件响应逻辑得到很好的比较好的管理
    至于代码好看不好看,其实都是看自己的编程风格而已啦~
    反正我是有代码洁癖 哈哈~


    啰嗦了这么多
    最后还是非常感谢看到这里的同学,笔者感觉到非常开心~

    困死了,眯一会去~


    不定期更新 不合适的地方 还请指点~ 感激不尽

    相关文章

      网友评论

        本文标题:基于 responder chain 的交互

        本文链接:https://www.haomeiwen.com/subject/spzinftx.html