TableView之Component

这种模式是我在上家公司的项目里用到的一种方式,整体来说就像是拼积木一样把各个展示组件、响应事件拆分出去,作为一个单独的类,然后通过工厂模式生产出来再组装展示。

在这里,我们把每个最小的封装组件叫做一个Component,每个事件的响应动作都叫做一个Action

约定

这种模式对于后台的依赖性很强,前端只需要封装好不同的小组件和大概的框架即可,由后台提供来规定布局的样式和响应的事件,所以,前后端就必须有一个完全统一的约定。
下面是json数据格式:

1
2
3
4
5
6
7
8
9
10
11
{
"component": {
# 这种类型下的数据资源,当然也能`component`嵌套
"action": {
# 响应动作
},
# 组件类型
"componentType": "word"
},
}

举个例子,这是评论内容的一个视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"component" : {
"action" : {
"actionType" : "detail",
"clearMsgNum" : "1",
"flag" : "-3",
"go_comments" : "1",
"id" : "480151",
"title" : "释然",
"type" : "thread"
},
"componentType" : "postsNewMessage",
"datetime" : "2016-11-02 17:44:26",
"description" : "峨眉山,云中花岭",
"messageCount" : "1",
"messageGroupId" : "480151",
"name" : "释然",
"picUrl" : "http://s3.mingxingyichu.cn/group6/M00/92/78/wKgBjFUDbQaADktKAAXsk4b7S1s41.jpeg?imageMogr2/quality/95",
"postSummary" : "http://ww2.sinaimg.cn/large/006AYr4pjw1f8s239mmx0j30m80de0ti.jpg",
"userFansNum" : "6"
},
"message_type" : "thread_msg"
}

我们在前端将模块分为了很多种,包括文字模块、图片模块、图文混排模块、视频播放模块、商品模块、推荐模块、标签模块等等数十上百个小模块,然后通过不同的嵌套达到页面展示的目的,如图(截取的是蘑菇街的图片,我们的APP貌似下架了):

创建基类

因为所有的可封装的组件都是约定好的,所以可以做一个基类对设定的数据做统一的分离提取

  • Component
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface CustomComponent : UIView
@property (nonatomic,strong) CustomAction *firedAction;
@property (nonatomic, weak) NSDictionary *detailStars;
@property (nonatomic, retain) NSDictionary *data;
-(id) initWithFrame:(CGRect)frame data:(NSDictionary *)data;
-(id) initWithContainer:(UIView *)container data:(NSDictionary *)data;
-(void) initUI;
-(CGFloat)getComponentHeightWithData:(NSDictionary *)data withRealWidth:(CGFloat)realWidth;
//点击action
-(void)fireAction;
-(void)fireActions:(NSInteger)index;
-(void)fireActionWith:(NSDictionary *)data;
@end

这里的点击事件或者根据Action自定义的事件是加在单独的Component里,一般都是加的一个点击的手势,其他的根据需求来做修改。
里面的具体实现就是View的定制,这里就不举例了。

  • Action
1
2
3
4
5
6
7
8
9
10
@interface CustomAction : NSObject
// 统计信息
@property (nonatomic, copy) NSDictionary *trackEventInfor;
-(id)initWith:(NSDictionary *)data;
-(void)initData;
-(void)fire;
@end

如视频播放的事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation ActionVideo
- (void)fire {
[super fire];
NSString *videoUrl = [_data objectForKey:@"videoUrl"];
if (![NSString isBlankString:videoUrl]) {
NSURL *movieURL = [NSURL URLWithString:videoUrl];
AutoRatoteMPMoviePlayerViewController *controller = [[AutoRatoteMPMoviePlayerViewController alloc] initWithContentURL:movieURL];
controller.delegateController = self.delegateNavigationController;
[controller play];
}
}
@end

创建工厂类

  • ComponentFactory

用于根据给定的数据创建每个小的组件

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@implementation ComponentFactory
+(CustomComponent *)createComponentWithFrame:(CGRect)frame data:(NSDictionary *)data navigation:(UINavigationController *)delegteNavigarionController{
NSString * componentStr = [ComponentFactory getComponentTypeWithData:data];
Class someClass = NSClassFromString(componentStr);
CustomComponent *cell = (CustomComponent *)[[someClass alloc] initWithFrame:frame data:data];
cell.delegateNavigationController = delegteNavigarionController;
return cell;
}
+(CGFloat )getComponentHeightWithData:(NSDictionary *)data withRealWidth:(CGFloat)realWidth{
// 根据给定的数据内容来计算当前模块的高度
}
+(NSString *)getComponentTypeWithData:(NSDictionary *)data{
NSString * componentStr = @"CustomComponent";
NSString *componentType = data[@"component"][@"componentType"];
if([@"word" isEqual:componentType]){
componentStr = @"ComponentWord";
}
if([@"videoCell" isEqual:componentType]){
componentStr = @"ComponentVideoCell";
}
if([@"calendar" isEqual:componentType]){
componentStr = @"ComponentCalendar";
}
if ([@"calendarWorthy" isEqualToString:componentType]) {
componentStr = @"ComponentCalendarWorthy";
}
// ...
return componentStr;
}
+(CustomComponent *)createComponentWithContainer:(UIView *)view data:(NSDictionary *)data navigation:(UINavigationController *)delegteNavigarionController{
CGRect frame = view.bounds;
CustomComponent *cell = [ComponentFactory createComponentWithFrame:frame data:data navigation:delegteNavigarionController];
return cell;
}
@end

可以看出,这里主要是把if-else的类型判断挪到这里来了。

  • ActionFactory
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@implementation ActionFactory
+(CustomAction *)createActionWithData:(NSDictionary *)data withDetaiStarData:(NSDictionary *)starDic navigation:(UINavigationController *)delegateNavigationController{
CustomAction *action;
if (![data isKindOfClass:[NSDictionary class]]) {
return nil;
}
NSString *actionType = data[@"actionType"];
if (!actionType) {
return nil;
}
NSString *type = data[@"type"];
NSString *child = data[@"child"];
if ([@"livingShow" isEqualToString:actionType]) { //直播播放
action = [[ActionLiveShow alloc] initWith:data];
}
if ([@"thread" isEqualToString:actionType]) {
if ([@"" isEqualToString:child]) {// 帖子列表
action = [[ActionThread alloc] initWith:data];
}
if ([@"topiclist" isEqualToString:child]) {// 专题列表
action = [[ActionTopicList alloc] initWith:data];
}
}
if ([@"detail" isEqual:actionType]) {
// 帖子详情页
if ([@"thread" isEqual:type]) {
action = [[ActionThreadDetail alloc]initWith:data];
}
// 用户空间详情页
if ([@"user" isEqual:type]) {
action=[[ActionSpace alloc] initWith:data];
}
}
if ([@"list" isEqualToString:actionType]) {
if ([@"msg" isEqualToString:type]) { //消息回复我的,社区通知,活动通知
action = [[ActionReplyMine alloc] initWith:data];
}
if ([@"msgEvent" isEqualToString:type]) {
action = [[ActionActivityNoticeDetail alloc]initWith:data];
}
}
return action;
}
@end

这里也是根据给定的数据来创建具体事件的实例。

页面使用实例

当然,由于数据的结构化,这完全可以做一层封装。

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
27
28
29
30
31
32
33
34
35
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return _data.count;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary *dic = self.data[indexPath.row];
NSInteger cellHeight = [ComponentFactory getComponentHeightWithData:dic withRealWidth:self.tableView.frame.size.width];
return cellHeight;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomComponent *component;
NSInteger row = [indexPath row];
NSDictionary *data = _data[row];
NSString * identifier= [NSString stringWithFormat:@"reuse%@", data[@"componentType"]];
CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if(cell == nil) {
cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
CGFloat cellHeight = [ComponentFactory getComponentHeightWithData:data withRealWidth:tableView.frame.size.width];
CGRect frame = CGRectMake(0, 0, tableView.frame.size.width, cellHeight);
component = [ComponentFactory createComponentWithFrame:frame data:data navigation:self.navigationController];
cell.component = component;
}
component.data = data;
component.row = row;
return cell;
}

总结

这种模式很适合阅读内容的展示,特别是文章类型的页面,由于页面的展示样式有后台控制,所以就提供了更多可定制的可能性。
上面也只是一种粗糙的代码展示,要想深究,也可以在很多细节上做优化。
不过缺点也很明显,后台依赖性很强,而且表格视图的交互性不好,对于内容的更改、cell位置的调整都不方便。
总之来说,这也是对于某种需求而产生的一种书写的方式,找对应用场景,做好优化,这也会给我们的APP提供丰富的功能和开发体验。

数据的依赖性太强,而且有原来项目现成的代码,就懒得写demo了,将就着看吧。