若是你以为 UITableViewDelegate
和 UITableViewDataSource
这两个协议中有大量方法每次都是复制粘贴,实现起来大同小异;若是你以为发起网络请求并解析数据须要一大段代码,加上刷新和加载后简直复杂度爆表,若是你想知道为何下面的代码能够知足上述全部要求:php
系好安全带,上车!面试
在讨论解耦以前,咱们要弄明白 MVC 的核心:控制器(如下简称 C)负责模型(如下简称 M)和视图(如下简称 V)的交互。json
这里所说的 M,一般不是一个单独的类,不少状况下它是由多个类构成的一个层。最上层的一般是以 Model
结尾的类,它直接被 C 持有。Model
类还能够持有两个对象:swift
常见的误区:设计模式
在 C 中,咱们建立 UITableView
对象,而后将它的数据源和代理设置为本身。也就是本身管理着 UI 逻辑和数据存取的逻辑。在这种架构下,主要存在这些问题:api
UITableView
自身完成的。为了解决这些问题,咱们首先弄明白,数据源和代理分别作了那些事。数组
它有两个必须实现的代理方法:缓存
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
简单来讲,只要实现了这个两个方法,一个简单的 UITableView
对象就算是完成了。安全
除此之外,它还负责管理 section
的数量,标题,某一个 cell
的编辑和移动等。网络
代理主要涉及如下几个方面的内容:
最经常使用的也是两个方法:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
提醒:绝大多数代理方法都有一个 indexPath
参数
最简单的思路是单独把数据源拿出来做为一个对象。
这种写法有必定的解耦做用,同时能够有效减小 C 中的代码量。然而总代码量会上升。咱们的目标是减小没必要要的代码。
好比获取每个 section
的行数,它的实现逻辑老是高度相似。然而因为数据源的具体实现方式不统一,因此每一个数据源都要从新实现一遍。
首先咱们来思考一个问题,数据源做为 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
其中的 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,这些都是数据源不该该作的事。
这两个问题的解决方案以下:
Cell
,子类视状况返回合适的类型。Cell
添加一个 setObject
方法,用于解析 Item 并更新 UI。通过这一番折腾,好处是至关明显的:
cellClassForObject
方法便可。原来的数据源方法已经在父类中被统一实现了。setObject
方法,而后坐等本身被建立,被调用这个方法便可。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
完成),咱们得到了如下特性:
UITableViewDelegate
)对照 demo(SHA-1:ca9b261),感觉一下效果。
在上面的两次封装中,其实咱们是把 UITableView
持有原生的代理和数据源,改为了 KtTableView
持有自定义的代理和数据源。而且默认实现了不少系统的方法。
到目前为止,看上去一切都已经完成了,然而实际上仍是存在一些能够改进的地方:
基于以上考虑, 咱们实现一个 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
该怎么用:
KtTableViewController
的视图控制器,而且调用它的 initWithStyle
方法。objc KTMainViewController *mainVC = [[KTMainViewController alloc] initWithStyle:UITableViewStylePlain];
在子类 VC 中实现 createDataSource
方法,实现数据源的绑定。
在数据源中,须要指定 cell 的类型。
在 Cell 中,须要经过解析数据,来更新 UI 并返回本身的高度。
* (CGFloat)tableView:(UITableView *)tableView rowHeightForObject:(KtTableViewBaseItem *)object { return 60; } // Demo 中沿用了父类的 setObject 方法。 ```
到目前为止,咱们实现了对 UITableView
以及相关协议、方法的封装,使它更容易使用,避免了不少重复、无心义的代码。
在使用时,咱们须要建立一个控制器,一个数据源,一个自定义 Cell,它们正好是基于 MVC 模式的。所以,能够说在封装与解耦方面,咱们已经作的至关好了,即便再花大力气,也很难有明显的提升。
但关于 UITableView
的讨论远远没有结束,我列出了如下须要解决的问题
关于第一个问题,实际上是普通的 MVC 模式中 V 和 C 的交互问题,能够在 Cell(或者其余类) 中添加 weak 属性达到直接持有的目的,也能够定义协议。
问题二和三是另外一大块话题,网络请求你们都会实现,但如何优雅的集成进框架,保证代码的简单和可拓展,就是一个值得深刻思考,研究的问题了。接下来咱们就重点讨论网络请求。
一个 iOS 的网络层框架该如何设计?这是一个很是宽泛,也超出我能力范围以外的问题。业内已有一些优秀的,成熟的思路和解决方案,因为能力,角色所限,我决定从一个普通开发者而不是架构师的角度来讲说,一个普通的、简单的网络层该如何设计。我相信再复杂的架构,也是由简单的设计演化而来的。
对于绝大多数小型应用来讲,集成 AFNetworking
这样的网络请求框架就足以应付 99% 以上的需求了。可是随着项目的扩大,或者用长远的眼光来考虑,直接在 VC 中调用具体的网络框架(下面以 AFNetworking
为例),至少存在如下问题:
AFNetworking
中止维护,并且咱们须要更换网络框架,这个成本将没法想象。全部的 VC 都要改动代码,并且绝大多数改动都是雷同的。这样的例子真实存在,好比咱们的项目中就依然使用早已中止维护的 ASIHTTPRequest
,能够预见,这个框架早晚要被替换。
ASIHTTPRequest
为例,它的底层用 NSOperation
来表示每个网络请求。众所周知,一个 NSOperation
的取消,并非简单调用 cancel
方法就能够的。在不修改源码的前提下,一旦它被放入队列,实际上是没法取消的。参考当前代码(SHA-1:a55ef42)感觉一下没有任何网络层时的设计。
其实解决方案很是简单:
全部的计算机问题,均可以经过添加中间层来解决
读者能够自行思考,为何添加中间层能够解决上述三个问题。
对于一个网络框架来讲,我认为主要有三个方面值得去设计:
一个完整的网络请求通常由以上三个模块组成,咱们逐一分析每一个模块实现时的注意事项:
发起请求时,通常有两种思路,第一种是把全部要配置的参数写到同一个方法中,借用 与时俱进,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 这个字典,为本身的属性赋值 // 具体的实现请见后面的文章 }
首先,咱们封装一个 KtBaseServerAPI
对象,这个对象的主要目的有三个:
具体的实现请参考 Git 提交历史:SHA-1:76487f7
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 的一个属性。
基类的 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
都具备下拉刷新和上拉加载的功能,在实现这个功能时,咱们主要考虑两点:
第一点已是老生常谈,参考 SHA-1 61ba974 就能够看到如何实现一个简单的封装。
重点在于对于 Model 和 Item 的改造。
这个 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
字段。经过这个字段,咱们同样能够判断是否加载到最后一页。
它持有一个 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 还应该肩负起去重的任务。为了简化问题,这里就不完整实现了。
它实现了 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 为了方便演示和阅读,一般都是先实现底层的类和方法,而后再由上层调用。但实际上这种作法在实际开发中是不现实的。咱们老是在发现大量冗余,无心义的代码后,才开始设计架构。
所以在我看来,真正的架构过程是当业务发生变动(一般是变复杂了)时,咱们开始应该思考当前哪些操做是能够省略的(由父类或代理实现),最上层应该以何种方式调用底层的服务。一旦设计好了最上层的调用方式,就能够逐步向底层实现了。
因为做者水平有限,本文的架构并不优秀,但愿在深刻理解设计模式,积累更多经验后,再与你们分享收获。
做为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个个人iOS交流群:1012951431 无论你是小白仍是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 你们一块儿交流学习成长!
另附上一份各好友收集的大厂面试题,进群可自行下载!