- 目录
- 前言
- 推导
- 实现
- 适用场景
- 栗子
- 优化
- 致谢
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
,而且使用这种交互,事件响应逻辑得到很好的比较好的管理
至于代码好看不好看,其实都是看自己的编程风格而已啦~
反正我是有代码洁癖 哈哈~
啰嗦了这么多
最后还是非常感谢看到这里的同学,笔者感觉到非常开心~
困死了,眯一会去~
不定期更新 不合适的地方 还请指点~ 感激不尽
网友评论