我们通常用的MVC,就会导致很多耦合度,也就是view拥有了model,还有就是好多东西都想Controller里面放,导致controller很臃肿,例如我们很多地方都用了
然后在cell里面展示数据,并在在controller里面写很多逻辑,现在给一种解耦的思路。
MVP架构
我们可以创建一个中间类来处理,将tableView代理方法和数据提取出来,但是在vc中不要有view的逻辑,已经数据处理的逻辑,vc只负责将mode传递给view,所以我们需要将UI和model绑定起来,然后通过代理、block等通讯,但是代理逻辑更清晰,block容易产生嵌套,嵌套深了,容易出现问题,并且block调试起来很麻烦,下面我们就以面向协议思想说明一下MVP架构:
定义一个协议:
@protocol PresentDelegate <NSObject>
@required
@optional
// 思维 : 面向协议编程 直播架构
// 接口 - 代理三部曲 - 以接口需求 <驱动代码>
// UI -> MODEL
- (void)didClickCellNum:(NSString *)num indexPath:(NSIndexPath *)indexPath;
- (void)reloadUI;
@end
然后让VC遵循这个协议,自己也遵循这个协议
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"MVP架构思路";
self.pt = [[Present alloc] init];
// self.pt.dataArray
// 多cell
// 矛盾
// 双向绑定 UI <-> model
// 通讯: 代理 MVP
__weak typeof(self) weakSelf = self;
self.dataSource = [[LMDataSource alloc] initWithIdentifier:reuserId configureBlock:^(MVCTableViewCell *cell, Model *model, NSIndexPath *indexPath) {
__strong typeof(weakSelf) strongSelf = weakSelf;
cell.nameLabel.text = model.name;
cell.numLabel.text = model.num;
cell.delegate = strongSelf.pt;
cell.indexPath = indexPath;
}];
self.view.backgroundColor = [UIColor whiteColor];
[self.view addSubview:self.tableView];
self.tableView.dataSource = self.dataSource;
self.pt.delegate = self;
[self.dataSource addDataArray:self.pt.dataArray];
// VC 过重 - 应该要放什么 - 解重
// 控制 - 分配任务 - 调和 (建立关联)
// 耦合度
}
在vc中创建一个pt对象,将vc设置为pt的代理,将pt设置为view的代理,这样通过代理就可以将mode和view双向绑定,当view中操作改变了model,就通过代理去修改pt里面的数据,假如pt里面的数据变了,就通过代理去刷新UI,这个就是MVP架构
架构二
还有一个Proxy架构,也介绍一下
1、在Controller里面定义两个属性
@property (nonatomic, strong) HomeDataSource *dataSource;
@property (nonatomic, strong) HomeTableViewProxy *tableViewProxy;
在创建tableView,并且将tableView的delegate和dataSource都设置为self.tableViewProxy,并且注册了不同类型的cell
- (UITableView *)tableView {
if (!_tableView) {
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
_tableView.delegate = self.tableViewProxy;
_tableView.dataSource = self.tableViewProxy;
_tableView.backgroundColor = LGBackgroundColor;
for (NSString *className in HomeTableViewCellIdentifiers.allValues) {
[self.tableView registerNib:[UINib nibWithNibName:className bundle:nil] forCellReuseIdentifier:className];
}
}
return _tableView;
}
其中HomeTableViewCellIdentifiers是一个字典,用于注册多种cell
#define HomeTableViewCellIdentifiers @{@"banner": @"HomeBannerTableViewCell",\
@"title": @"HomeTitleTableViewCell",\
@"one": @"HomeOneBigTableViewCell",\
@"two": @"HomeTwoTableViewCell",\
@"change": @"HomeChangeTableViewCell",\
@"rank": @"HomeRankTableViewCell"}
然后再加载数据
- (void)loadData{
self.dataSource = [HomeDataSource new];
[self.dataSource getChannel:@"11" completion:^(BOOL succeed, NSError *error, id data) {
HomeTemplateResponse *response = (HomeTemplateResponse *)data;
if (response && response.data.count) {
self.errorView.hidden = YES;
} else {
self.errorView.hidden = NO;
}
self.tableViewProxy.dataArray = [NSArray arrayWithArray:response.data];
[self.tableView reloadData];
}];
}
HomeDataSource的getChannel方法其实就是获取数据的方法:
- (void)getChannel:(NSString *)channelId completion:(void (^)(BOOL, NSError *, id))cBlock {
NSString *path = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"Home_TableView_Response_%@", channelId] ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:path];
NSError *error = nil;
HomeTemplateResponse *response = [[HomeTemplateResponse alloc] initWithData:data error:&error];
if (cBlock) cBlock(YES, error, response);
}
我们发现拿到数据后会将数据给self.tableViewProxy.dataArray,然后刷新tableView,我们再看一下HomeTableViewProx里面的实现,其中HomeTableViewProx里面实现了需要实现的tabelView代理方法
- (instancetype)initWithReuseIdentifier:(NSString *)reuseIdentifier configuration:(LGConfigBlock)cBlock action:(LGActionBlock)aBlock {
if (self = [super init]) {
_reuseIdentifier = reuseIdentifier;
_cellConfigBlock = cBlock;
_cellActionBlock = aBlock;
}
return self;
}
#pragma mark tableview delegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
HomeTemplateItem *item = self.dataArray[indexPath.row];
return [[HomeTableViewCellHeights objectForKey:item.templateName] floatValue];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.cellActionBlock) {
self.cellActionBlock([tableView cellForRowAtIndexPath:indexPath], self.dataArray[indexPath.row], indexPath);
}
}
#pragma mark tableview data source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
HomeTemplateItem *item = self.dataArray[indexPath.row];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[HomeTableViewCellIdentifiers objectForKey:item.templateName] forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.contentView.backgroundColor = LGBackgroundColor;
if (self.cellConfigBlock) self.cellConfigBlock(cell, self.dataArray[indexPath.row], indexPath);
return cell;
}
其中HomeTableViewCellHeights是个字典,针对不同cell的高度
#define HomeTableViewCellHeights @{@"banner": @(150*LGScreenWidth/320),\
@"title": @(40),\
@"one": @(250*LGScreenWidth/375),\
@"two": @(127*LGScreenWidth/320),\
@"change": @(40),\
@"rank": @(210*LGScreenWidth/375)}
里面cell的区分是通过返回的数据来判断展示不同的cell以及设定不同的cellHeight
我们再看一下tableViewProxy的创建
- (HomeTableViewProxy *)tableViewProxy {
if (!_tableViewProxy) {
_tableViewProxy = [[HomeTableViewProxy alloc] initWithReuseIdentifier:@"HomeTableViewCell" configuration:^(LGTableViewCell *cell, id cellData, NSIndexPath *indexPath) {
if ([cell isKindOfClass:[LGTableViewCell class]]) {
[(LGTableViewCell *)cell configWithData:cellData];
}
} action:^(LGTableViewCell *cell, id cellData, NSIndexPath *indexPath) {
HomeTemplateItem *item = (HomeTemplateItem *)cellData;
[LGMediator jumpToPlayerVC:item.templateData.firstObject];
}];
}
return _tableViewProxy;
}
这样我们就发现当刷新tableView时候就会调用
这是因为使用的cell都是LGTableViewCell的子类,并且实现子类展示数据的方法,我们看一下其中的一个实现:
- (void)configWithData:(id)data {
if ([data isKindOfClass:[HomeTemplateData class]]) {
HomeTemplateData *item = (HomeTemplateData *)data;
self.titleLabel.text = item.title;
self.subTitleLabel.text = item.subTitle;
[self.imageView setImageWithURL:[NSURL URLWithString:item.img]];
}
}
首先我们确定传进来的数据对象是该cell所需要的数据对象,然后在这里面展示数据就可以了
我们再看一下点击事件,我们发现再点击时候都会走
这样我们就完成了初步的解耦,引入了Proxy来实现tableView的代理方法,减轻了VC,我们再试着更轻量化一下VC
适配器架构
上面架构,很多内容相似的页面怎么办呢,,我们定义一个Adapter,在这里面处理tableView的代理方法等
在controller里面先执行这个方法
configMVP是在controller的基类里面,我们看看的实现:
// view customView loadView
- (void)configMVP:(NSString*)name
{
self.mvpEnabled = true;
self.rootContext = [[CDDContext alloc] init]; //strong
self.context = self.rootContext; //weak
//presentor
Class presenterClass = NSClassFromString([NSString stringWithFormat:@"KC%@Presenter", name]);
if (presenterClass != NULL) {
self.context.presenter = [presenterClass new];
self.context.presenter.context = self.context;
}
//interactor
Class interactorClass = NSClassFromString([NSString stringWithFormat:@"KC%@Interactor", name]);
if (interactorClass != NULL) {
self.context.interactor = [interactorClass new];
self.context.interactor.context = self.context;
}
//view
Class viewClass = NSClassFromString([NSString stringWithFormat:@"KC%@View", name]);
if (viewClass != NULL) {
self.context.view = [viewClass new];
self.context.view.context = self.context;
}
//build relation
self.context.presenter.view = self.context.view;
// self(vc) -weak-> context -> self
self.context.presenter.baseController = self;
self.context.interactor.baseController = self;
self.context.view.presenter = self.context.presenter;
self.context.view.interactor = self.context.interactor;
}
- (void)viewDidLoad
{
[super viewDidLoad];
if (self.mvpEnabled) {
self.context.view.frame = self.view.bounds;
self.view = self.context.view;
}
// assign : weak nil
// 监控网络 + 页面统计
KCLog(@"\n\nDid Load ViewController: %@\n\n", [self class]);
}
在这个地方上下文是作为处理各种事项的处理,因为有时候在view中需要拿到vc的一些数据,或者需要vc本身,所以我们创建一个上下文,也就是中间层来去处理这些逻辑,
- 首先我们先说明控制器使用了适配器mvp架构
- 为控制器创建了一个上下文,为了避免循环引用,在NSObject的分类中创建了一个弱属性context,但是对外宣称是强引用,这样在context持有self的时候就不会造成强引用:
- 因为vc强持有了rootContext,会让然后有个弱指针指向了这个对象
- 这个弱指针又去持有了self,这样当self的引用计数不会+1,这一就打破了持有链。不会造成循环引用了
- 在设置完上下文后,会在viewDidLoad再将上下文的view替换掉VC的view
- (void)setContext:(CDDContext*)object {
objc_setAssociatedObject(self, @selector(context), object, OBJC_ASSOCIATION_ASSIGN);
}
- (CDDContext*)context {
id curContext = objc_getAssociatedObject(self, @selector(context));
if (curContext == nil && [self isKindOfClass:[UIView class]]) {
//try get from superview, lazy get
UIView* view = (UIView*)self;
UIView* sprView = view.superview;
while (sprView != nil) {
if (sprView.context != nil) {
curContext = sprView.context;
break;
}
sprView = sprView.superview;
}
if (curContext != nil) {
[self setContext:curContext];
}
}
return curContext;
}
在获取上下文时候,如果获取对象是view并且该view没有上下文,我们会遍历view的父类直到能找到能处理事项的view为止
- 在创建完上下文后,我们可以看看上下文的内容:
#import "NSObject+CDD.h"
@class CDDContext;
@class CDDView;
@interface CDDPresenter : NSObject
@property (nonatomic, weak) UIViewController* baseController;
@property (nonatomic, weak) CDDView* view;
@property (nonatomic, weak) id adapter; //for tableview adapter
@end
@interface CDDInteractor : NSObject
@property (nonatomic, weak) UIViewController* baseController;
@end
@interface CDDView : UIView
@property (nonatomic, weak) CDDPresenter* presenter;
@property (nonatomic, weak) CDDInteractor* interactor;
@end
//Context bridges everything automatically, no need to pass it around manually
@interface CDDContext : NSObject
@property (nonatomic, strong) CDDPresenter* presenter;
@property (nonatomic, strong) CDDInteractor* interactor;
@property (nonatomic, strong) CDDView* view; //view holds strong reference back to context
@end
其中CDDPresenter是处理数据用的基类
CDDInteractor是处理业务逻辑的的基类
CDDView是用来展示的view的基类
CDDContext是上下文的基类
我们在viewDidLoad里面创建上下文,然后通过runtime获取到VC对应的Presenter、Interactor、View的对象,然后持有,所有我们在创建vc的这几个对象时候要根据命名规则来创建,不能随便创建,并且这几个对象都要若持有上下文,这样就形成了双向链表
创建完上下文以及涉及到的对象后,我们在创建一个协议,我们在给vc一个属性homeAdapter,他是作为tableView代理而存在的,我们看一下KCHomeAdapter的实现:
@implementation KCHomeAdapter
- (CGFloat)getCellHeight:(NSInteger)row
{
CGFloat height = SCREEN_WIDTH*608/1080 + 54;
KCChannelProfile *model = [self.getAdapterArray objectAtIndex:row];
if (model.title.length > 0) {
CGSize titleSize = [model.title sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:14]}];
if (titleSize.width > SCREEN_WIDTH - 35) {
// 两行
height +=67;
}else{
height +=50;
}
}else{
height += 8;
}
return height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self getCellHeight:indexPath.row];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.getAdapterArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
KCChannelProfile* liveModel = self.getAdapterArray[indexPath.row];
UITableViewCell *cell = nil;
//这个地方可以加个判断,根据model的类型展示不同的cell
CCSuppressPerformSelectorLeakWarning (
cell = [self performSelector:NSSelectorFromString([NSString stringWithFormat:@"tableView:cellForKCChannelProfile:"]) withObject:tableView withObject:liveModel];
);
return cell;
}
// base 基础,可以写多种这样的方法对应不同的cell
// home floww mine
// cell 多种 cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForKCChannelProfile:(id)model {
NSString *cellIdentifier = NSStringFromSelector(_cmd);
KCHomeTableViewCell *cell = (KCHomeTableViewCell *)[tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[KCHomeTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
}
KCChannelProfile* liveModel = model;
[cell setCellContent:liveModel];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:NO];
id model = self.getAdapterArray[indexPath.row];
if (self.adapterDelegate && [self.adapterDelegate respondsToSelector:@selector(didSelectCellData:)]) {
[self.adapterDelegate didSelectCellData:model];
}
}
其中如果是多cell,我们可以定义多个- (UITableViewCell *)tableView:(UITableView *)tableView cellForKCChannelProfile:(id)model 这种方法去展示,在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath方法里面通过不同mode去展示不同的cell
再通过宏KC去加载页面
KC的宏定义:
#define KC(instance, protocol, message) [(id<protocol>)(instance) message]
这是一个面向协议的思想,就是让view都遵循KCHomePresentDelegate协议,然后去调用协议的方法buildHomeView:并且将vc的homeAdapter传进去,在vc的基类的viewDidLoad中,我们已经将vc的view替换成自定义的view了,所以我们可以直接去KCHomeView看下
KCHomeView的实现:
@interface KCHomeView ()
@property (nonatomic, strong) UITableView *tableView;
@end
@implementation KCHomeView
- (instancetype)init{
self = [super init];
if (self) {
self.backgroundColor = [UIColor orangeColor];
}
return self;
}
- (void)buildHomeView:(KCHomeAdapter *)adapter {
// UI 的事情
CGRect frame = self.bounds;
frame.size.height -= 44;
self.tableView = [[UITableView alloc] initWithFrame:frame];
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self addSubview:_tableView];
_tableView.delegate = adapter;
_tableView.dataSource = adapter;
// 我要数据
KC(self.presenter, KCHomePresentDelegate, loadDataWithAdapter:adapter);
}
- (void)reloadUIWithData{
[_tableView reloadData];
}
@end
在view里创建了需要展示的view,这里是tableView,然后将传进来的vc的homeAdapter设置为tableView的代理,再去加载数据,加载方式依然用宏KC去加载,这个时候就用到了view的presenter,我们知道view的presenter就是上下文的presenter,所以我们去KCHomePresenter里面看看,
@class KCBaseAdapter;
@protocol KCHomePresentDelegate <NSObject>
@optional
// 通过adapter建立我们的视图 : 主要是数据与视图的绑定
- (void)buildHomeView:(KCBaseAdapter *)adapter;
// 加载数据
- (void)loadDataWithAdapter:(KCBaseAdapter *)adapter;
// UI去刷新界面了
- (void)reloadUIWithData;
// 去直播间
- (void)gotoLiveStream;
@end
@interface KCHomePresenter : CDDPresenter
@property (nonatomic, strong) NSMutableArray *dataArray;
@end
#import "KCHomePresenter.h"
#import "KCChannelProfile.h"
#import "KCHomeAdapter.h"
@implementation KCHomePresenter
- (instancetype)init{
if (self == [super init]) {
self.dataArray = [NSMutableArray arrayWithCapacity:10];
}
return self;
}
- (void)loadDataWithAdapter:(KCBaseAdapter *)adapter{
NSMutableArray* dataArray = @[].mutableCopy;
for (int i = 0; i < 20; i ++) {
KCChannelProfile* channel = [KCChannelProfile new];
channel.ownerName = @"Logic";
channel.title = @"姿势从未如此性感,学习从未如此快乐";
channel.ownerLocation = @"湖南逻辑教育";
channel.userCount = @(10000);
if (i%3 == 0) {
channel.ownerCover = @"publicPicture";
}else if (i%3 == 1){
channel.ownerCover = @"adance";
}else{
channel.ownerCover = @"reverse";
}
[dataArray addObject:channel];
}
[adapter setAdapterArray:dataArray];
// 数据来了 - 刷新UI
KC(self.view,KCHomePresentDelegate,reloadUIWithData);
}
#pragma mark - lazy
- (NSMutableArray *)dataArray{
if (!_dataArray) {
_dataArray = [NSMutableArray arrayWithCapacity:10];
}
return _dataArray;
}
我们看到在KCHomePresenter里面去获取数据,并且处理数据,因为KCHomePresenter也持有了对应的view(都是在vc的基类中自动绑定的),所以获取完数据去,再把数据给传过来的vc的homeAdapter,然后让view去刷新UI,同样通过面向协议思想,这个时候view的tabelView的代理是homeAdapter,并且此时homeAdapter已经拿到了数据,此时让View去刷新数据完全没问题
总结:
我们总结一下:
- viewDidLoad时候设置上下文,并且将上下文presenter、view、interactor都创建好,并且将vc的view替换成自己创建的View,并且让他们互相持有
- 给vc创建KCHomeAdapter对象,这个对象是用来给View传递数据的中间类,然后让VC的view去创建自己
- 创建完成后将需要数据的view和KCHomeAdapter对象绑定起来,然后通过presenter去加载数据
- 在presente中获取完数据后给KCHomeAdapter对象,然后让view去刷新UI
按照上面逻辑就完全解耦了controller、view、model,三者之间的关系通过中间层context来完成:
- 其中controller的homeAdapter只负责代理和从presenter拿到数据传递给view
- presenter只负责获取数据以及数据的处理
- view只负责刷新UI,展示数据
- 如果还有其他业务数据,我们可以创建一个interactor来处理业务数据