- 目录
- 前言
- 推导
- 实现
- 适用场景
- 栗子
- 优化
- 致谢
1. 前言
抱歉,最近事情有些烦,文章没有更新
这几天在着手重构公司产品的首页模块,真的是不想吐槽接手的这份代码,简直就是一坨粑粑,完全没有架构和设计模式可言,有些模块所有的logic、view、action基本全都写在C中,可以看下面截图
某个C
当然👆这个图,主要并不是意在代码行数,而是在于这个C保存状态的数量
这个纯属吐槽,等项目几个主要模块重构完成,单独写一篇对 “天下武功出少林” 的MVC的使用
2. 推导
基于MVC,V的事件响应需要传递到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]}]
但是这种写法
如果需要在当前view的nextresponder中继续向上传递的话
则需要在当前view的nextresponder中调用
[super ff_routerEventWithEventName:eventName userInfo:userInfo]
综上,笔者在重构的时候选择的是实现1,其实无论是1还是2,只要直接找到目标responder,其实都没影响,不需要调用super
4. 适用场景
我们知道,在V和C的交互中,V经常使用protocol或block或其他来将响应传递到C
此时V可以分为两种情况(以cell和代理为例)
-
V的布局简单,使用一次代理即可(如下图,cell中的btn需要传递响应事件到C)
-
V的布局复杂(如下图,cell中有自定义的VA,VA中有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 chain在C调用即可如下
那么为什么需要把userinfo的引用放在2位置呐?
因为方法调用的时候,有两个默认参数,0->id self,1->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,而且使用这种交互,事件响应逻辑得到很好的比较好的管理
至于代码好看不好看,其实都是看自己的编程风格而已啦~
反正我是有代码洁癖 哈哈~
啰嗦了这么多
最后还是非常感谢看到这里的同学,笔者感觉到非常开心~
困死了,眯一会去~
不定期更新 不合适的地方 还请指点~ 感激不尽

某个C
权威解释
权威解释













网友评论