基于Responder Chain的对象交互

问题

在直播间中对视图进行解耦的时候,遇到了一个问题:

因为是将具体业务视图放到对应的contentView中,并且这些业务视图的事件接收方还是当前视图控制器,就会造成事件的传递需要透过contentView这一层。

如果中间只有contentView这一层还好,可以使用一个多余的delegate或者block将事件转换一下,但如果业务视图自己中也有其他的子控件需要传递事件到视图控制器,那就不止一层了。在软件开发中,只有变和不变,在这里就是如果他有一层,那么就可能有n多层,为了一层提出的方案在n多层中就会显得不那么适用,因为这样并没有解决根本问题。

当然使用通知可以无视事件触发层和处理层之间的距离,但是,通知在我看来不是一个很好的UI通信方式,并且满天飞的通知很难管理。

normal

先说一下以前都是怎么处理多层事件传递的,主要有两种方式。

第一种做法是为contentView添加一个代理,这个代理继承至其子视图们的代理,由于协议是可以多继承,因此可以这么写,但是这样就会暴露这个contentView内部的子视图,不符合封装的特性,没有很好的体现这个contentView的封装性。

这样的做法比较省心,不需要写很多的无用代理方法,缺点就是上面提到的暴露了内部的类。

另一种做法是在contentView内部对子视图做一个代理传递,自己统一代理协议的接口:

内部将子视图的代理回调方法传递给自己的代理:

这个做法的好处一个是代理接口统一,另外就是可以在子视图的代理回调里面做一些contentView的业务处理,灵活性更高一些。

缺点就是需要写好多代理方法以及传递子视图的代理给contentVIew的代理,并且如果嵌套层级过深写起来就会很不优雅。

那么有没有一个很优雅的方法来解决这个难题呢?

Responder Chain

这个方案是从casatwy那里看到的,基于响应链来实现,具体介绍可以参考作者的文章,主要思路就是为UIResponder添加一个分类方法,将要传递的数据交给其nextResponder,直到多层之后的控制器,控制器可以重写该分类方法来实现具体的业务逻辑。

由于在响应链中事件的传递是自上而下的,也就是先从点击的控件开始再到其所在的contentView,然后再到LivingRoomVC。

这个方案可以解决多UI层级下的事件传递,但是不能解决反向数据代理,比如深层的UI需要控制器通过代理(确切点叫数据源)来返回数据,当然在业务中比较多的还是控件之间的事件传递,而且作者也说了这个模式只能处理基于响应链的事件传递。

基于响应链的方案,只需要为UIResponder添加一个分类方法,将对应的事件交给nextResponder,因此中间的响应者还可以根据业务需求针对不同的事件进行添加或者修改等操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface UIResponder (EventRouter)

- (void) routerEventWithName:(NSString *)eventName;
- (void) routerEventWithName:(NSString *)eventName userInfo:(nullable NSDictionary *)userInfo;

@end

@implementation UIResponder (EventRouter)

- (void) routerEventWithName:(NSString *)eventName userInfo:(nullable NSDictionary *)userInfo{
if (nil != eventName) {
[[self nextResponder] routerEventWithName:eventName userInfo:userInfo];
}
}

@end

将业务事件使用一个EventProxy抽离于视图控制器之外,内部使用一个集合来存储策略,然后在视图控制器将对应事件交给EventProxy的时候做消息转发,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@interface EventProxy()
@property (nonatomic, copy) NSSet<NSValue *> *eventStrategy;
@end

@implementation EventProxy

- (void)handleEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo{

__block NSDictionary * strongUserInfo = userInfo;
[self.eventStrategy enumerateObjectsUsingBlock:^(NSValue * _Nonnull obj, BOOL * _Nonnull stop) {
// EventWrap是一个用来包裹event-action的私有结构体
EventWrap event = EventWrapFromNSValue(obj);
if ([event.name isEqualToString:eventName]) {
NSInvocation *invocation = [self createInvocationWithSelector:event.method];
if (invocation) {
if (invocation.methodSignature.numberOfArguments > 2) {
[invocation setArgument:&strongUserInfo atIndex:2];
}
[invocation invoke];
}
*stop = YES;
}
}];
}

@end

视图控制器的子视图通过直接调用-routerEventWithName:userInfo:方法发送事件,可选针对事件传递一些附加数据:

1
2
3
4
// in a subView of ViewController .m
- (void) onButton{
[self routerEventWithName:@"sub-view-button-click"];
}

子类化一个EventProxy,并且预先添加好所有的业务策略,然后成为视图控制器的属性,在控制器的-routerEventWithName:userInfo:方法中将对应的事件交给子类EventProxy,由于父类EventProxy中已经在-handleEvent:userInfo:方法中做了对应的事件映射,所以这里可以直接将子类创建的action和对应的event进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@implementation XXXLivingRoomEventProxy

-(instancetype)initWithController:(__kindof UIViewController *)controller{
self = [super init];
if (self) {
self.controller = controller;
_eventStrategy = [NSSet setWithArray:({
@[NSValueFromEventAndMethod(@"sub-view-button-click",
@selector(onSubViewButtonClick))
})];
}
return self;
}

- (void) onSubViewButtonClick{
NSLog(@"onSubViewButtonClick");
[self.controller presentViewController:({
UIAlertController * alert = [UIAlertController alertControllerWithTitle:@"Title" message:@"Message" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:({
[UIAlertAction actionWithTitle:@"Sure" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
NSLog(@"alert sure");
}];
})];
alert;
}) animated:YES completion:nil];
}

参考文章