假如你觉得 UITableViewDelegate 和 UITableViewDataSource 这两个协议中有很多办法每次都是复制粘贴,完结起来迥然不同;假如你觉得建议网络恳求并解析数据需求一大段代码,加上改写和加载后几乎复杂度爆表,假如你想知道为什么下面的代码能够满意上述一切要求:

知道如何根据业务去优化UITableView,你会感觉到工作无比顺畅

解耦后的VC

MVC

在评论解耦之前,咱们要弄明白 MVC 的中心:操控器(以下简称 C)担任模型(以下简称 M)和视图(以下简称 V)的交互。 这儿所说的 M,一般不是一个单独的类,许多状况下它是由多个类构成的一个层。最上层的一般是以 Model 结尾的类,它直接被 C 持有。Model 类还能够持有两个方针:

  1. Item:它是实践存储数据的方针。它能够理解为一个字典,和 V 中的特点一一对应
  2. Cache:它能够缓存自己的 Item(假如有许多) 常见的误区:
  3. 一般状况下数据的处理睬放在 M 而不是 C(C 只做不能复用的事)
  4. 解耦不只是把一段代码拿到外面去。而是重视是否能合并重复代码, 而且有良好的拖展性。

原始版

在 C 中,咱们创立 UITableView 方针,然后将它的数据源和署理设置为自己。也便是自己管理着 UI 逻辑和数据存取的逻辑。在这种架构下,首要存在这些问题:

  1. 违反 MVC 形式,现在是 V 持有 C 和 M。
  2. C 管理了悉数逻辑,耦合太严重。
  3. 其实绝大多数 UI 相关都是由 Cell 而不是 UITableView 自身完结的。 为了处理这些问题,咱们首要弄明白,数据源和署理别离做了那些事。 数据源 它有两个有必要完结的署理办法:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

简略来说,只需完结了这个两个办法,一个简略的 UITableView 方针就算是完结了。 除此以外,它还担任管理 section 的数量,标题,某一个 cell 的编辑和移动等。 署理 署理首要触及以下几个方面的内容:

  1. cell、headerView 等展现前、后的回调。
  2. cell、headerView 等的高度,点击事情。 最常用的也是两个办法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

提醒:绝大多数署理办法都有一个 indexPath 参数

优化数据源

最简略的思路是单独把数据源拿出来作为一个方针。 这种写法有一定的解耦作用,一起能够有效削减 C 中的代码量。但是总代码量会上升。咱们的方针是削减不必要的代码。 比方获取每一个 section 的行数,它的完结逻辑总是高度相似。但是因为数据源的详细完结办法不一致,所以每个数据源都要重新完结一遍。

SectionObject

首要咱们来考虑一个问题,数据源作为 M,它持有的 Item 长什么样?答案是一个二维数组,每个元素保存了一个 section 所需求的悉数信息。因而除了有自己的数组(给cell用)外,还有 section 的标题等,咱们把这样的元素命名为 SectionObject:

@interface KtTableViewSectionObject : NSObject
@property (nonatomic, copy) NSString *headerTitle; // UITableDataSource 协议中的 titleForHeaderInSection 办法或许会用到
@property (nonatomic, copy) NSString *footerTitle; // UITableDataSource 协议中的 titleForFooterInSection 办法或许会用到
@property (nonatomic, retain) NSMutableArray *items;
- (instancetype)initWithItemArray:(NSMutableArray *)items;
@end

Item

其间的 items 数组,应该存储了每个 cell 所需求的 Item,考虑到 Cell 的特点,基类的 BaseItem 能够规划成这样:

@interface KtTableViewBaseItem : NSObject
@property (nonatomic, retain) NSString *itemIdentifier;
@property (nonatomic, retain) UIImage *itemImage;
@property (nonatomic, retain) NSString *itemTitle;
@property (nonatomic, retain) NSString *itemSubtitle;
@property (nonatomic, retain) UIImage *itemAccessoryImage;
- (instancetype)initWithImage:(UIImage *)image Title:(NSString *)title SubTitle:(NSString *)subTitle AccessoryImage:(UIImage *)accessoryImage;
@end

父类完结代码

规矩好了一致的数据存储格式以后,咱们就能够考虑在基类中完结某些办法了。以 – (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 办法为例,它能够这样完结:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.sections.count > section) {
        KtTableViewSectionObject *sectionObject = [self.sections objectAtIndex:section];
        return sectionObject.items.count;
    }
    return 0;
}

比较困难的是创立 cell,因为咱们不知道 cell 的类型,天然也就无法调用 alloc 办法。除此以外,cell 除了创立,还需求设置 UI,这些都是数据源不应该做的事。 这两个问题的处理方案如下:

  1. 界说一个协议,父类回来基类 Cell,子类视状况回来适宜的类型。
  2. 为 Cell 增加一个 setObject 办法,用于解析 Item 并更新 UI。

优势

经过这一番折腾,好处是适当显着的:

  1. 子类的数据源只需求完结 cellClassForObject 办法即可。原来的数据源办法现已在父类中被一致完结了。
  2. 每一个 Cell 只需写好自己的 setObject 办法,然后坐等自己被创立,被调用这个办法即可。
  3. 子类经过 objectForRowAtIndexPath 办法能够快速获取 item,不必重写。 对照 demo(SHA-1:6475496),感受一下作用。

优化署理

咱们以之前所说的,署理协议中常用的两个办法为例,看看怎么进行优化与解耦。 首要是核算高度,这个逻辑并不一定在 C 完结,因为触及到 UI,所以由 Cell 担任完结即可。而核算高度的依据便是 Object,所以咱们给基类的 Cell 加上一个类办法:

+ (CGFloat)tableView:(UITableView*)tableView rowHeightForObject:(KtTableViewBaseItem *)object;

另外一类问题是以处理点击事情为代表的署理办法, 它们的首要特点是都有 indexPath 参数用来表示方位。但是实践在处理过程中,咱们并不联系方位,关怀的是这个方位上的数据。 因而,咱们对署理办法做一层封装,使得 C 调用的办法中都是带有数据参数的。因为这个数据方针能够从数据源拿到,所以咱们需求能够在署理办法中获取到数据源方针。 为了完结这一点, 最好的办法便是承继 UITableView:

@protocol KtTableViewDelegate<UITableViewDelegate>
@optional
- (void)didSelectObject:(id)object atIndexPath:(NSIndexPath*)indexPath;
- (UIView *)headerViewForSectionObject:(KtTableViewSectionObject *)sectionObject atSection:(NSInteger)section;
// 将来能够有 cell 的编辑,交流,左滑等回调
// 这个协议承继了UITableViewDelegate ,所以自己做一层中转,VC 仍然需求完结某
@end
@interface KtBaseTableView : UITableView<UITableViewDelegate>
@property (nonatomic, assign) id<KtTableViewDataSource> ktDataSource;
@property (nonatomic, assign) id<KtTableViewDelegate> ktDelegate;
@end

cell 高度的完结如下,调用数据源的办法获取到数据:

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath {
    id<KtTableViewDataSource> dataSource = (id<KtTableViewDataSource>)tableView.dataSource;
    KtTableViewBaseItem *object = [dataSource tableView:tableView objectForRowAtIndexPath:indexPath];
    Class cls = [dataSource tableView:tableView cellClassForObject:object];
    return [cls tableView:tableView rowHeightForObject:object];
}

优势 经过对 UITableViewDelegate 的封装(其实首要是经过 UITableView 完结),咱们获得了以下特性:

  1. C 不必关怀 Cell 高度了,这个由每个 Cell 类自己担任
  2. 假如数据自身存在数据源中,那么在署理协议中它能够被传给 C,免去了 C 重新拜访数据源的操作。
  3. 假如数据不存在于数据源,那么署理协议的办法会被正常转发(因为自界说的署理协议承继自 UITableViewDelegate) 对照 demo(SHA-1:ca9b261),感受一下作用。

愈加 MVC,愈加简练

在上面的两次封装中,其实咱们是把 UITableView 持有原生的署理和数据源,改成了 KtTableView 持有自界说的署理和数据源。而且默认完结了许多体系的办法。 到目前为止,看上去一切都现已完结了,但是实践上仍是存在一些能够改进的地方:

  1. 目前仍然不是 MVC 形式!
  2. C 的逻辑和完结仍然能够进一步简化 基于以上考虑, 咱们完结一个 UIViewController 的子类,而且把数据源和署理封装到 C 中。
@interface KtTableViewController : UIViewController<KtTableViewDelegate, KtTableViewControllerDelegate>
@property (nonatomic, strong) KtBaseTableView *tableView;
@property (nonatomic, strong) KtTableViewDataSource *dataSource;
@property (nonatomic, assign) UITableViewStyle tableViewStyle; // 用来创立 tableView
- (instancetype)initWithStyle:(UITableViewStyle)style;
@end

为了保证子类创立了数据源,咱们把这个办法界说到协议里,而且界说为 required。

成果与方针

现在咱们整理一下经过改造的 TableView 该怎么用:

  1. 首要你需求创立一个承继自 KtTableViewController 的视图操控器,而且调用它的 initWithStyle 办法。 objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];
  2. 在子类 VC 中完结 createDataSource 办法,完结数据源的绑定。
*   (void)createDataSource { self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 这 一步创立了数据源 } ```

1.在数据源中,需求指定 cell 的类型。

*   (Class)tableView:(UITableView *)tableView cellClassForObject:(KtTableViewBaseItem *)object { return [KtMainTableViewCell class]; } 

1.在 Cell 中,需求经过解析数据,来更新 UI 并回来自己的高度。

*   (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父类的 setObject 办法。 

还有什么要优化的 到目前为止,咱们完结了对 UITableView 以及相关协议、办法的封装,使它更容易运用,避免了许多重复、无意义的代码。 在运用时,咱们需求创立一个操控器,一个数据源,一个自界说 Cell,它们正好是基于 MVC 形式的。因而,能够说在封装与解耦方面,咱们现已做的适当好了,即便再花大力气,也很难有显着的提高。 但关于 UITableView 的评论远远没有完毕,我列出了以下需求处理的问题

  1. 在这种规划下,数据的回传不够便利,比方 cell 的给 C 发消息。
  2. 下拉改写与上拉加载怎么集成
  3. 网络恳求的建议,与解析数据怎么集成 关于第一个问题,其实是一般的 MVC 形式中 V 和 C 的交互问题,能够在 Cell(或者其他类) 中增加 weak 特点到达直接持有的意图,也能够界说协议。 问题二和三是另一大块话题,网络恳求我们都会完结,但怎么高雅的集成进结构,保证代码的简略和可拓展,便是一个值得深化考虑,研究的问题了。接下来咱们就要点评论网络恳求。

#为何创立网络层

一个 iOS 的网络层结构该怎么规划?这是一个十分广泛,也超出我能力范围之外的问题。业界已有一些优异的,成熟的思路和处理方案,因为能力,人物所限,我决议从一个一般开发者而不是架构师的角度来说说,一个一般的、简略的网络层该怎么规划。我信任再复杂的架构,也是由简略的规划演化而来的。 关于绝大多数小型运用来说,集成 AFNetworking 这样的网络恳求结构就足以敷衍 99% 以上的需求了。但是跟着项意图扩大,或者用长远的眼光来考虑,直接在 VC 中调用详细的网络结构(下面以 AFNetworking 为例),至少存在以下问题:

  1. 一旦日后 AFNetworking 停止保护,而且咱们需求替换网络结构,这个本钱将无法想象。一切的 VC 都要改动代码,而且绝大多数改动都是雷同的。 这样的例子实在存在,比方咱们的项目中就仍然运用早已停止保护的 ASIHTTPRequest,能够预见,这个结构迟早要被替换。
  2. 现有的结构或许无法完结咱们的需求。以 ASIHTTPRequest 为例,它的底层用 NSOperation 来表示每一个网络恳求。众所周知,一个 NSOperation 的撤销,并不是简略调用 cancel 办法就能够的。在不修改源码的前提下,一旦它被放入行列,其实是无法撤销的。
  3. 有时候咱们的需求仅仅是进行网络恳求,还会对这个恳求进行各种自界说的拓展。比方咱们或许要统计恳求的建议和完毕时间,从而核算网络恳求,数据解析的步骤的耗时。有时候,咱们期望规划一个通用组件,而且支持由各个事务部分去自界说详细的规矩。比方或许不同的部分,会为 HTTP 恳求增加不同的头部。
  4. 网络恳求还有或许有其他广泛需求增加的需求,比方恳求失败时的弹窗,恳求时的日志记载等等。 参阅当时代码(SHA-1:a55ef42)感受一下没有任何网络层时的规划。

怎么规划网络层

其实处理方案十分简略:

一切的核算机问题,都能够经过增加中间层来处理

读者能够自行考虑,为什么增加中间层能够处理上述三个问题。

三大模块

关于一个网络结构来说,我认为首要有三个方面值得去规划:

  1. 怎么恳求
  2. 怎么回调
  3. 数据解析

一个完好的网络恳求一般由以上三个模块组成,咱们逐个剖析每个模块完结时的注意事项:

建议恳求

建议恳求时,一般有两种思路,第一种是把一切要配置的参数写到同一个办法中,借用与时俱进,HTTP/2下的iOS网络层架构规划一文中的代码表示:

+ (void)networkTransferWithURLString:(NSString *)urlString
                       andParameters:(NSDictionary *)parameters
                              isPOST:(BOOL)isPost
                        transferType:(NETWORK_TRANSFER_TYPE)transferType
                   andSuccessHandler:(void (^)(id responseObject))successHandler
                   andFailureHandler:(void (^)(NSError *error))failureHandler {
                           // 封装AFN
                   }

这种写法的好处在于一切参数一望而知,而且简略易用,每次都调用这个办法即可。但是缺点也很显着,跟着参数和调用次数的增多,网络恳求的代码很快多到爆破。 另一组办法则是将 API 设置成一个方针,把要传入的参数作为这个方针的特点。在建议恳求时,只需设置好方针的相关特点,然后调用一个简略的办法即可。

@interface DRDBaseAPI : NSObject
@property (nonatomic, copy, nullable) NSString *baseUrl;
@property (nonatomic, copy, nullable) void (^apiCompletionHandler)(_Nonnull id responseObject,  NSError * _Nullable error);
- (void)start;
- (void)cancel;
...
@end

依据前文提到的 Model 和 Item 的概念,那么应该能够想到:这个用于拜访网络的 API 方针,其实是作为 Model 的一个特点

Model 担任对外露出必要的特点和办法,而详细的网络恳求则由 API 方针完结,一起 Model 也应该持有真实用来存储数据的 Item。

怎么回调

一次网络恳求的回来成果应该是一个 JSON 格式的字符串,经过体系的或者一些开源结构能够将它转换成字典。

接下来咱们需求运用 runtime 相关的办法,将字典转换成 Item 方针。

最终,Model 需求将这个 Item 赋值给自己的特点,从而完结整个网络恳求。

假如从大局角度来说,咱们还需求一个 Model 恳求完结的回调,这样 VC 才干有机会做相应的处理。

考虑到 Block 和 Delegate 的优缺点,咱们挑选用 Block 来完结回调。

数据解析

这一部分首要是利用 runtime 将字典转换成 Item,它的完结并不算难,但是怎么隐藏好完结细节,使上层事务不必过多关怀,则是咱们需求考虑的问题。

咱们能够界说一个基类的 Item,而且为它界说一个parseData函数:

// KtBaseItem.m
- (void)parseData:(NSDictionary *)data {
    // 解析 data 这个字典,为自己的特点赋值
    // 详细的完结请见后面的文章
}

封装 API 方针

首要,咱们封装一个 KtBaseServerAPI 方针,这个方针的首要意图有三个:

  1. 隔离详细的网络库的完结细节,为上层供给一个稳定的的接口
  2. 能够自界说一些特点,比方网络恳求的状况,回来的数据等,便利的调用
  3. 处理一些共用的逻辑,比方网络耗时统计 详细的完结请参阅 Git 提交前史:SHA-1:76487f7

Model 与 Item

###BaseModel Model 首要需求担任建议网络恳求,而且处理回调,来看一下基类的 Model 怎么界说:

@interface KtBaseModel
// 恳求回调
@property (nonatomic, copy) KtModelBlock completionBlock;
//网络恳求
@property (nonatomic,retain) KtBaseServerAPI *serverApi;
//网络恳求参数
@property (nonatomic,retain) NSDictionary *params;
//恳求地址 需求在子类init中初始化
@property (nonatomic,copy)   NSString *address;
//model缓存
@property (retain,nonatomic) KtCache *ktCache;

它经过持有 API 方针完结网络恳求,能够定制自己的存储逻辑,操控恳求办法的挑选(长、短链接,JSON或protobuf)。 Model 应该对上层露出一个十分简略的调用接口,因为假定一个 Model 对应一个 URL,其实每次恳求只需求设置好参数,就能够调用适宜的办法建议恳求了。 因为咱们不能预知恳求何时完毕,所以需求设置恳求完结时的回调,这也需求作为 Model 的一个特点。

BaseItem

基类的 Item 首要是担任 property name 到 json path 的映设,以及 json 数据的解析。最中心的字典转模型完结如下:

- (void)parseData:(NSDictionary *)data {
    Class cls = [self class];
    while (cls != [KtBaseItem class]) {
        NSDictionary *propertyList = [[KtClassHelper sharedInstance] propertyList:cls];
        for (NSString *key in [propertyList allKeys]) {
            NSString *typeString = [propertyList objectForKey:key];
            NSString* path = [self.jsonDataMap objectForKey:key];
            id value = [data objectAtPath:path];
            [self setfieldName:key fieldClassName:typeString value:value];
        }
        cls = class_getSuperclass(cls);
    }
}

完好代码参阅 Git 提交前史:SHA-1:77c6392

怎么运用

在实践运用时,首要要创立子类的 Modle 和 Item。子类的 Model 应该持有 Item 方针,而且在网络恳求回调时,将 API 中带着的 JSON 数据赋值给 Item 方针。 这个 JSON 转方针的过程在基类的 Item 中完结,子类的 Item 在创立时,需求指定特点名和 JSON 路径之间的对应联系。 关于上层来说,它需求生成一个 Model 方针,设置好它的路径以及回调,这个回调一般是网络恳求回来时 VC 的操作,比方调用 reloadData 办法。这时候的 VC 能够确定,网络恳求的数据就存在 Model 持有的 Item 方针中。 详细代码参阅 Git 提交前史:SHA-1:8981e28

下拉改写

许多运用的 UITableview 都具有下拉改写和上拉加载的功能,在完结这个功能时,咱们首要考虑两点: 1 隐藏底层的完结细节,对外露出稳定易用的接口 2 Model 和 Item 怎么完结 第一点现已是老生常谈,参阅 SHA-1 61ba974 就能够看到怎么完结一个简略的封装。 要点在于关于 Model 和 Item 的改造。

###ListItem 这个 Item 没有什么别的作用,便是界说了一个特点 pageNumber,这是需求与服务端洽谈的。Model 将会依据这个特点这个特点判别有没有悉数加载完。

// In .h
@interface KtBaseListItem : KtBaseItem
@property (nonatomic, assign) int pageNumber;
@end
// In .m
- (id)initWithData:(NSDictionary *)data {
    if (self = [super initWithData:data]) {
        self.pageNumber = [[NSString stringWithFormat:@"%@", [data objectForKey:@"page_number"]] intValue];
    }
    return self;
}

关于 Server 来说,假如每次都回来 page_number 无疑是十分低效的,因为每次参数都或许不同,核算总数据量是一项十分耗时的作业。因而在实践运用中,客户端能够和 Server 约定,回来的成果中带有 isHasNext 字段。经过这个字段,咱们一样能够判别是否加载到最终一页。

ListModel

它持有一个 ListItem 方针, 对外露出一组加载办法,而且界说了一个协议 KtBaseListModelProtocol,这个协议中的办法是恳求完毕后将要执行的办法。

@protocol KtBaseListModelProtocol <NSObject>
@required
- (void)refreshRequestDidSuccess;
- (void)loadRequestDidSuccess;
- (void)didLoadLastPage;
- (void)handleAfterRequestFinish; // 恳求完毕后的操作,改写tableview或封闭动画等。
@optional
- (void)didLoadFirstPage;
@end
@interface KtBaseListModel : KtBaseModel
@property (nonatomic, strong) KtBaseListItem *listItem;
@property (nonatomic, weak) id<KtBaseListModelProtocol> delegate;
@property (nonatomic, assign) BOOL isRefresh; // 假如为是,表示改写,否则为加载。
- (void)loadPage:(int)pageNumber;
- (void)loadNextPage;
- (void)loadPreviousPage;
@end

实践上,当 Server 端产生数据的增删时,只传 nextPage 这个参数是不能满意要求的。两次获取的页面并非彻底没有交集,很有或许他们具有重复元素,所以 Model 还应该肩负起去重的使命。为了简化问题,这儿就不完好完结了。

RefreshTableViewController

它完结了 ListMode 中界说的协议,供给了一些通用的办法,而详细的事务逻辑则由子类完结。

#pragma -mark KtBaseListModelProtocol
- (void)loadRequestDidSuccess {
    [self requestDidSuccess];
}
- (void)refreshRequestDidSuccess {
    [self.dataSource clearAllItems];
    [self requestDidSuccess];
}
- (void)handleAfterRequestFinish {
    [self.tableView stopRefreshingAnimation];
    [self.tableView reloadData];
}
- (void)didLoadLastPage {
    [self.tableView.mj_footer endRefreshingWithNoMoreData];
}
#pragma -mark KtTableViewDelegate
- (void)pullUpToRefreshAction {
    [self.listModel loadNextPage];
}
- (void)pullDownToRefreshAction {
    [self.listModel refresh];
}

#实践运用 在一个 VC 中,它只需求承继 RefreshTableViewController,然后完结 requestDidSuccess 办法即可。下面展现一下 VC 的完好代码,它超乎寻常的简略:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self createModel];
    // Do any additional setup after loading the view, typically from a nib.
}
- (void)createModel {
    self.listModel = [[KtMainTableModel alloc] initWithAddress:@"/mooclist.php"];
    self.listModel.delegate = self;
}
- (void)createDataSource {
    self.dataSource = [[KtMainTableViewDataSource alloc] init]; // 这一步创立了数据源
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}
- (void)requestDidSuccess {
    for (KtMainTableBookItem *book in ((KtMainTableModel *)self.listModel).tableViewItem.books) {
        KtTableViewBaseItem *item = [[KtTableViewBaseItem alloc] init];
        item.itemTitle = book.bookTitle;
        [self.dataSource appendItem:item];
    }
}

其他的判别,比方恳求完毕时封闭动画,最终一页提示没有更多数据,下拉改写和上拉加载触发的办法等公共逻辑现已被父类完结了。 详细代码见 Git 提交前史:SHA-1:0555db2 写在结尾 网络恳求的规划架构到此就悉数完毕了,它还有许多值的拓展的地方。仍是那句老话,没有通用的架构,只要最适合事务的架构。 我的 Demo 为了便利演示和阅览,一般都是先完结底层的类和办法,然后再由上层调用。但实践上这种做法在实践开发中是不现实的。咱们总是在发现很多冗余,无意义的代码后,才开端规划架构。 因而在我看来,真实的架构过程是当事务产生改变(一般是变复杂了)时,咱们开端应该考虑当时哪些操作是能够省掉的(由父类或署理完结),最上层应该以何种办法调用底层的服务。一旦规划好了最上层的调用办法,就能够逐渐向底层完结了。 因为自己水平也有限,本文的架构并不优异,期望在深化理解规划形式,积累更多经验后,再与我们分享收获。

青山不改,绿水常流。谢谢我们!