iOS解耦架构

2,015 阅读10分钟

我们通常用的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来处理业务数据