iOS开发笔记之六十四——基于UIView模块化组件方案


******阅读完此文,大概需要20分钟******
一、方案背景

有这么一个需求,需要做一个展示信息详情页,内容可能会比较多,超过一屏,最终还需要生成一张完整的详情页截图(UIImage), 如果我们直接基于UIScrollView(UITableView)去截屏,只会生成UIScrollView的frame的size大小的图片,不能生成contentSize大小的图片,所以,我们需要基于UIView去实施截图,有人可能会问,再UIView上再去放一个UIScrollView(UITableView),要知道截屏操作本身就是一个性耗比较大的操作,经测试验证,直接基于UIView去截屏性耗比最小。所以,需要采取UIScrollView+UIView去设计这个页面的层级。待内容页展示完成,可以直接对UIView去截屏。 内容详情页页一般设计信息较多、布局复杂、开发工作量大,所以模块化划分这个页面十分有必要。按照已有的模块化思想,创建一个ViewCtroller统一管理它要展示的子模块,管理包括统一数据刷新、统一布局等;


二、方案的设计与实现

截止这篇wiki开始,所有的代码都已经开发完成,并已经在公司的很多项目中实践,完整的组件代码地址如下:

https://github.com/lizitao000/MDViewModules.git

读者可以自己去下载并debug,下面我来简单介绍一下这个组件的原理与使用。

基类:MDBaseModuleViewController,在基类VC中有个contentBgView,在这个contentBgview中,我们批量将模块化的子view进行添加,如下:

- (NSArray *)loadContentViews
{
return @[@"MDDemoHeadModuleView",
@"MDDemoBottomModuleView",
@"MDDemoMiddleModuleView",
@"MDDemoHeadModuleView",
@"MDDemoMiddleModuleView",
];
}

既然要统一管理这些模块,就需要抽象出一层模块的基类,包含一些必须的公共操作,如下:

在这些公共操作中,有统一刷新布局(替代layoutsubviews)、有统一分发数据的操作,为了便于管理,还给每个模块统一标示了一个index。所以我们的模块基类MDBaseModuleView只需要实现这个protocol,并实现它的公共操作;为什么要统一封装这些操作?目的其实很简单,是因为layoutsubviews或者viewdidlayoutsubviews都会有一些不必要执行,大量的布局代码写到其中,是一种不明智的做法。

当然,有了基类的另一个好处,就是我们可以将一些模块反复使用的、重复的代码封装到基类中去,精简代码。

2、所有modules共享一份model数据

模块通过- (void)loadViewWithData:(id)model;方法将model带入每个模块,每个模块取自己需要的数据。如果模块需要“留存”一些value给下次操作来用,建议单独创建并赋值给property变量。

三、刷新布局的优化

当VC的model数据拿到时,所有的页面模块都会共享一份model,每个子模块从model中取自己需要的数据,计算自己的布局,如下:

- (void)loadAllSubviewsData
{
[self.contentBgView.subviews enumerateObjectsUsingBlock:^(__kindof UGCPBaseModuleView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj conformsToProtocol:@protocol(UGCPBaseModuleDelegate)]) {
[obj configViewWithIndex:idx];
[obj loadViewWithData:self.model];
}
}];
[self bindAllSubViewsHeight];
}
- (void)layoutAllSubviews{    __block CGFloat layoutOffestY = 0.0;    @weakify(self);    [self.contentBgView.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {        @strongify(self);        if ([obj conformsToProtocol:@protocol(UGCPBaseModuleDelegate)]) {            [obj layoutViewWithWidth:[self contentBgViewWidth]];            obj.top = layoutOffestY + [self spacingBetweenSubviews];            obj.left = [self leftSpacingOfSubviews];            layoutOffestY = floor(obj.bottom);        }    }];    self.contentBgView.frame = CGRectMake(0, 0, self.view.width, layoutOffestY);}

我们在起始可以统一调用这两个方法,但是仅仅这样统一操作是不够的。如果某个模块中有异步数据(图片或者子接口请求等),需要再次刷新这个模块布局,也就是我们可以像tableview的reloadData一样,再次执行layoutAllSubviews方法,把整个页面的所有模块再次刷新一遍,此时模块复杂度为O(n)。事实上,每个模块的布局变化,并不总是需要刷新整个布局,如下:

页面的各个模块,固定的宽度、固定的x坐标、top依赖上个模块的bottom:


当其中一个模块(Module 1)的布局发生变化时(高度height增加或减少,bottom改变),如下:


我们只需要按需更新那些依赖module 1的模块(modile 1下面的modules)的布局即可,如下:


此时刷新模块布局的平均复杂度为O(n/2)。

具体实现时候,我们首先要给每个模块一个唯一的标示index(0、1、2、3),通过protocol方法初始化时带进每个模块:

- (void)configViewWithIndex:(NSUInteger)index;

其次,还需要给每个模块构造一个可以监控模块height变化的信号RACSignal(RACSubject包含发送信号操作),我们只需要在模块里面构造height变化的信号,
这个信号要发送到主VC类,通知VC类哪个模块(index)的高度发生了变化,从而“定向”按需刷新页面布局,模块绑定信号如下:
- (void)configViewWithIndex:(NSUInteger)index
{
self.index = index;
[[[[RACObserve(self, height) distinctUntilChanged] skip:1] deliverOnMainThread] subscribeNext:^(id x) {
[self.heightChangeSignal sendNext:[NSNumber numberWithInteger:index]];
}];
}
VC类负责merge这些信号,并处理:
- (void)bindAllSubViewsHeight
{
__block RACSignal *signal = [RACSubject subject];
[self.contentView.subviews enumerateObjectsUsingBlock:^(__kindof MDBaseModuleView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
RACSubject *s = obj.heightChangeSignal;
if (idx == 0) {
signal = s;
} else {
signal = [signal merge:s];
}
}];
@weakify(self);
[[[signal distinctUntilChanged] skip:0] subscribeNext:^(id x) {
@strongify(self);
NSLog(@"---------->%@",x);
}];
}

拿到height变化的模块index之后,我们就可以轻松根据index布局这个页面了:
- (void)relayoutSubViewsWithIndex:(NSUInteger)index
{
__block CGFloat layoutOffestY = [self.contentBgView.subviews objectAtIndex:index].bottom;
[self.contentBgView.subviews enumerateObjectsUsingBlock:^(__kindof UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (idx > index) {
obj.top = layoutOffestY + [self spacingBetweenSubviews];
layoutOffestY = floor(obj.bottom);
}
}];
self.contentBgView.frame = CGRectMake(0, 0, self.view.width, layoutOffestY);
}

 

四、支持Module后端动态可配

最新的代码支持模块化后台动态可配,只需要实现如下三个方法即可:

- (BOOL)isSupportDynamicConfigration

- (NSArray *)dynamicModules

- (NSDictionary *)allDynamicModules

如果你的页面模块有需要将各个模块进行动态管理(添加、删除、调整顺序等),这个将是个很好的选择。你可以从后端接口中获取模块的Identify序列,这个功能会根据此序列生成各个模块类。当然,所有的模块与identify的字典需要提前在App中注册好。 

五、总结

总结一下,此组件具有以下特点:

1、统一化的模块化管理,继承基类,开发者无需投入太多时间在页面布局上,开发效率高;代码复用性高,代码比起传统开发较少;

2、大量的子view的布局代码无需写在layoutSubviews与viewdidlayoutSubviews中,提升页面布局效率;

3、按需“定向”刷新布局(平均复杂度O(n/2))性能高于传统统一刷新效率平均复杂度(O(n));







注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
© 2014-2019 ITdaan.com 粤ICP备14056181号