四月 10, 2018

UITableView针对可变高度Cell的架构设计(上)

针对UITableView可变高度Cell的问题,可以先从需求出发再整理一些问题点,举个今日头条的例子。

  1. 需要显示N种不同类型的Feed内容
  2. 不同Feed内容对应不同的模板

上面两点只是产品需求层面的,但是对于开发者两说,这里面涉及的东西就多了

  1. 需要一个数据类来表示Feed数据,但是由于涉及到不同的Feed内容对应的数据结构有可能是不一样的,因此可能需要定义N种Feed数据类
  2. 需要一个type字段来区别不同的Feed
  3. 不同的Feed对应的模板是不一样的,那么需要针对各种Feed数据类额外配套一个UITableViewCell的子类
  4. 计算各种feed对应Cell的高度(这个是重点)

总结下来就是:
1. Feed数据的定义
2. 模板(UITableViewCell)的设计
3. 整合到UITableView

针对以上的总结,我们对于不同的部分需要采取不同的架构设计。其实这里说架构设计可能有点夸张了,更多的是一种解决方案吧。下面就针对不同的部分分别阐述

Feed数据的定义

数据定义是一个非常重要的部分,而且必须要跟服务端开发人员一起配合才能达到完美的设计,什么是最完美的也没一个标准,因此就拿我在项目中的实际经验来举个例子。

不管是什么种类的Feed,首先是抽取不同种类Feed中的公共部分,拿今日头条来举例,有feedtype、来源、发布时间等,那么大可以将这些数据定义在基类中(BaseFeedData)。

其实影响数据结构定义的除了数据本身,还和使用何种数据传输方式有关。如果是使用传统的JSON结构来做数据传输,那么可以直接在JSON中体现数据结构,而如果是采用protoclbuffer作为数据传输手段的话,那么就要考虑PB不支持继承等问题。为了简单介绍,下面就按JSON来介绍。

由于采用JSON格式作为数据传输,那么在定义JSON的时候本身是无需考虑类的继承等因素的,但是数据是需要提供给客户端使用的,因此在定义JSON结构的时候还是需要考虑下客户端如何方便的将数据转换成对应的数据类的,这个过程叫做DTO(Data to Object)。所以,尽可能的将通用数据定义在同一个数据层级,然后对于不同Feed需要的不同数据可以放入另外一个层级,这样有一个好处,客户端在DTO的时候对于通用数据可以直接通过定义一个通用数据类就能解析了。而对于剩下的非通用数据完全可以采用不同的方式来解析。下面贴出一段JSON结构的数据。

[{
        "type": 1,
        "commonDataObject": {
            "from": "abc",
            "time": "1231245"
        },
        "feedContent": {
            "title": "hello"
        }
    },
    {
        "type": 2,
        "commonDataObject": {
            "from": "abc",
            "time": "1231245"
        },
        "feedContent": {
            "desc": "abcd"
        }
    }
]

针对以上情况其实也有很多种处理方式。

  1. 直接采用子类继承的方式来定义
  2. 采用类扩展来实现。
  3. 不解析,直接采用dictionary来做源数据。

在实际的项目中,对于Feed的定义其实是很复杂的,里面包括了很多的数据,因此如果直接采用dictionary的方式来做源数据的话,那么在实际的开发过程中会遇到各种问题。因此这里就直接不采用。

而对于前面两种方式,不管是在iOS还是在java上面,在实际的解析过程中是很麻烦的,因为你要知道这是在解析一个数组,而且数组包含的是不同的Feed数据结构,考虑到java没有扩展类的API(kotlin倒是支持的)因此对于java来说更优的方式采用第一种方式。而对于iOS来说,采用类扩展的方式是一种更优的解决方案,这里主要是考虑了iOS没有提供DTO类库以及没有提供泛型支持。下面单独说下在iOS中这样的数据类如何定义。

其实就算采用了类扩展的方式,针对不同的Feed内容还是需要定义各自不同的数据类的,而之所以不直接采用继承的方式定义,那是因为在iOS中DTO本身就是一件非常复杂、非常恶心的事情。iOS中不像C#、JAVA提供了一整套的DTO的类库来给开发者使用,iOS中只是很简单的提供了一个将JOSN数据转换成NSDictionary、NSArray的NSJSONSerialization,甚至连泛型也不提供,这压根就不能DTO。至于如何在iOS中实现DTO我会在下一篇中集中介绍下原理和实现过程。对于泛型支持的问题,那些在其他高级语言中使用过泛型技术的朋友肯定对泛型这个技术有深刻的使用心得,对于泛型在架构设计中的重要作用也会有深刻的理解。但是无奈iOS中没有提供泛型支持(swift语言是提供了泛型支持的,但是跟其他高级语言提供的泛型支持度来说还是有一定的差距的)。

综上,采用扩展类的方式,可以将复杂的DTO的过程交给各自的扩展类来实现,而且可以做到只需要在用到的时候才去做DTO处理。采用扩展类的好处是,你可以像泛型那样使用一个数据类。

下面直接贴出代码。

@interface BaseFeedData : NSObject

/**
 feed类型
 */
@property (nonatomic,assign)NSInteger feedType;

/**
 通用数据对象,每个Feed都会有
 */
@property (nonatomic,strong)NSObject *commonDataObject;

/**
 feed内容。直接使用NSDictionary
 */
@property (nonatomic,strong)NSDictionary *feedContentDict;

/**
 feed内容实际对应的数据对象
 */
@property (nonatomic,strong)id feedContentObject;
@end


@implementation BaseFeedData
@end

BaseFeedData这个类定义了一个通用的Feed数据结构,其中两个字段需要注意下。
1. feedContentDict:这里直接将feedContent定义成字典类型,主要是为了方便在扩展类中做DTO处理。
2. feedContentObject:这个字段主要是存放经过DTO后的实际对应的feed数据类。避免每次使用数据的时候都重新解析一遍。

/**
 定义feedtype为1的feed数据类
 */
@interface Type1FeedContent : NSObject
@property (nonatomic,strong)NSString *title;
@end

@implementation Type1FeedContent
@end

上面是定义了一个feedtype为1的feed数据类。为了举例方便只定义了一个title的字段

@interface BaseFeedData (ExtensionType1)
@property (nonatomic,strong,readonly)Type1FeedContent *type1Content;
@end

@implementation BaseFeedData (ExtensionType1)
-(Type1FeedContent *)type1Content{
    if(self.feedContentObject==nil){
        // 将feedContentDict 转换成 Type1FeedContent
        self.feedContentObject = DTO(self.feedContentDict,[Type1FeedContent class]);
    }
    return self.feedContentObject;
}
@end

上面的代码定义了一个feedtype为1 的数据扩展类。为什么要定义一个这样的扩展类?其实从里面的字段定义来看就能看出,主要是为了方便获取实际对应的type1Content以及对数据进行DTO,另外一个作用就是可以当泛型来使用。事实上,由于ObjC是弱类型的,因此就算你没有定义这么一个类,也可以采用KVC的方式来获取你想要的数据,但是无论如何都没有多定义一个这样一个扩展类来的方便,何况这个扩展类你可以直接定义在某个.m文件中(前提是这个数据只有这个.m文件才会用到)。另外这样的定义方式,由于ObjC语言的特殊性(弱类型),并不会多生成一个类。你之所以能这样定义,可以说完全是XCode的功劳,这个跟ARC的实现原理差不多。

Posted in iOS

1 Comment

发表评论

电子邮件地址不会被公开。 必填项已用*标注