2018-04-24

2018-04-24  本文已影响0人  hubjf

app架构之应用层架构

期望

view和model是可以复用的,业务逻辑是可以测试的。模块的代码应该职责清晰便于后期维护和扩展。

应用层架构定义

一个颇具规模的app必然会涉及到组件化、分层设计、公共模块等等。如下图:


componentFactory.png

组件化方案: 目前采用工APFFactory库,通过trrgierEvent事件通知、goPage页面跳转、dataProvider数据获取进行组件间的交互、数据共享。

公共模块: 数据库 采用FMDB,网络库采用restDao,流量监控等

三层划分: 应用层,service层,data access层

下面讨论的主要是针对应用层

MVC模式

MVC.png

优点

缺点

推荐架构

MVVM/MVCVM模式

tableViewMVVM.png

优点

代码例子

一个很常见的业务功能,如下图:

recentChat.jpg

注解

这是一个典型的列表场景,用tableView视图实现,但是如果把所有业务包括cellView的业务逻辑都堆积到一个MVVM架构中显然会显得臃肿不合适,因此会把他拆分tableView、controller、tablViewModel为一个MVVM架构模式,其中每个tableCellView又是一个小型的MVVM架构如图:
[图片上传失败...(image-d844f2-1524550781161)]

但是这里有个比较大的问题,tableView的mvvm和tableCellView的mvvm如何衔接起来哪?众所周知苹果的tableCellView是有复用机制的如图:


tablecache1.png

即tableCellView只会创建屏幕能显示的个数,而cellViewModel会创建完整的数量。那协调器cellCoordinator的他们直接的关系哪?如果cellCoordinator与cellViewmodel保持一一对应那么会有两个问题,有两份内存,对cellViewModel的增删同时要对应的对cellCoordinator做增。其二当cell超出屏幕被放入回收池,cellCoordinator不知道解绑。因此cellCoordinator应该与cellView一一对应,即用一个NSDictory保存key是cellView,value是cellCoordinator。这样既能减少内存占用,在cellView复用的时候能找到对应的cellCoordinator不能解除绑定。如下图:


tableViewCache2.png

具体代码如下:

1.1 ViewController & View

@interface RecentViewController ()<UITableViewDelegate,
                                    UITableViewDataSource>
@property(nonatomic, strong) RecentViewModel *viewModel;
@property(nonatomic, strong) UITableView *tableView;
@property(nonatomic, strong) NSDictionary *coordinatorDic; //用于保存cell协调器,用于和cell一一对应
@end

@implementation RecentViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self addSubView];
    [self bindData];
    [self fetchData];
}

- (void)addSubView {
    self.tableView = [[UITableView alloc] initWithFrame:CGRectZero];
    self.tableView.tableFooterView = [[UIView alloc] init];
    self.tableView.backgroundColor = [UIColor clearColor];
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    
    [self.view addSubview:self.tableView];
    [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
}
- (void)bindData {
    [self.kvoController observe:self.viewModel keyPath:@"cellViewModelList" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial block:^(id observer, id object, NSDictionary *change) {
        @strongify(self);
        [self.tableView reloadData];
    }];
}

- (void)fetchData {
    [self.viewModel fetchData];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return [self.viewModel.cellViewModelList count];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    id viewModel = MUPArrayObjectAtIndex(self.viewModel.cellViewModelList, indexPath.row);
    return [viewModel cellHeight];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    id viewModel = MUPArrayObjectAtIndex(self.viewModel.cellViewModelList, indexPath.row);
    NSString *identifier = @"ChatPlainTextCell";
    RecentCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    if (!cell) {
        cell = [[RecentCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:identifier];
    }
    RecentCellCoordinator *cellCoordinator = = [self getCoordinatorWithCell:cell];
    [cellCoordinator bind:viewModel];
    return cell;
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    RecentCellCoordinator *cellCoordinator = [self.coordinatorDic objectForKey:cell];
    [cellCoordinator didSelectCellView];
}
@end

- (void)getCoordinatorWithCell:(UITableViewCell *)cell {
    RecentCellCoordinator *cellCoordinator = [self.coordinatorDic objectForKey:cell];
    if (!cellCoordinator) {
        cellCoordinator = [[RecentCellCoordinator alloc] initWithCellView:cell];
        [self.coordinatorDic setObject:cellCoordinator forKey:cell];
    }
    return cellCoordinator;
}

1.2 ViewModel & Model

@interface RecentViewModel
@property (nonatomic, strong, readonly) NSArray *cellViewModelList;  //Model
- (void)fetchData;
@end
@interface RecentViewModel ()
@property (nonatomic, strong) NSArray *cellViewModelList; 
@end

@implementation RecentViewModel

- (void)fetchData {
    //从数据源(dataSource类)获取数据
    NSArray *array =  [self.dataSource fetchDataList];
    //将数据转换为CellViewModel
    self.dataList = [self convertToCellViewModels:array];
}

- (NSArray *)convertToCellViewModels:(NSArray *)array {

    NSMutableArray *cellVMArray = [NSMutableArray array];
    for (int i=0; i<[array count]; i++) {
        Model *model = array[i];
        RecentCellViewModel *cellViewModel = [TableCellViewModelFactory createCellViewModel:model];

        [cellVMArray addObject:cellViewModel];
    }
    return cellVMArray;
}

@end

这里面的RecentViewModel只做业务逻辑,而视图的绑定和事件代理者有RecentViewController负责,因此RecentViewModel是可以测试。


1.1.1 CellView

@protocol SWTableViewCellDelegate
- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index;
@end

@protocol RecentCellDelegate
@optional
- (void)onAvatarClick:(NSInterger)uid;

@end

@interface RecentCell
@property(nonatomic, assign) NSInterger uid;
@property(nonatomic, strong) NSString *title;
@property(nonatomic, strong) NSString *timeText;
@property(nonatomic, strong) NSString *content;
@property(nonatomic, strong) NSString *rightButtonTitle;
@property(nonatomic, weak) id<SWTableViewCellDelegate> swDelegate;
@property(nonatomic, weak) id<RecentCellDelegate> delegate;
@end

1.1.2 CellViewModel & Model

@interface RecentCellViewModel
@property(nonatomic, strong) NSString *title;
@property(nonatomic, strong) NSString *timeText;
@property(nonatomic, strong) NSString *content;
@property(nonatomic, assign) BOOL isFollowing;
- (instancetype)initWithConversation:(IMSConversation *)conv;
- (void)follow;
@end
@implementation RecentCellViewModel
- (void)follow {
    if(_isFollowing) {
        self.isFollowing = YES;
        [PSPManager ShareInstance] unFollow:_uid complete:^(NSError *error, id result) {
            if (error) { self.isFollowing = NO; }//follow失败 状态回退
            //...略
        }
    } else {
        //flow PSP
    }
}
@end

1.1.3 协调器,负责绑定View和ViewModel

@interface RecentCellCoordinator () <SWTableViewCellDelegate,RecentCellDelegate>
@property(nonatomic, weak) RecentCell *cell;
@property(nonatomic, weak) RecentCellViewModel *cellViewModel;
@property(nonatomic, strong) FBKVOController *kvoController;
@end
@implementation RecentCellCoordinator
- (instanceType)initWithCellView:(RecentCell *)cell {
    if (self = [super init]) {
        self.cell = cell;
        self = cell.delegate;
        self = cell.swDelegate;
    }
}
- (void)bind:(RecentCellViewModel *)viewModel {
    self.cellViewModel = viewModel;
    self.cell.title = viewModel.title;
    //...  设置其他属性

    [kvoController observe:viewModel keyPath:@"isFollowing" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial block:^(id observer, id object, NSDictionary *change) {
        //修改recentCell的展示内容
        self.cell.rightButtonTitle = self.viewModel.isFollowing ? @"取消关注" : @"关注";
    }];
}

- (void)swipeableTableViewCell:(SWTableViewCell *)cell didTriggerRightUtilityButtonWithIndex:(NSInteger)index {
    
    [self.cellViewModel follow];
}

- (void)onAvatarClick:(NSInterger)uid {
    //调转个人主页
    PersonMainController *ctl = [[PersonMainController allooc] initWithUid:uid];
    //push ctl;
}
@end

这里每个cell又是一个独立的MVVM,其业务和胶水代码写在自己的模块中。其中RecentCell只做布局和渲染,并将点击代理开给CellCoordinator,RecentCell的复用性贼好。而RecentCellViewModel制作业务逻辑测试性高,RecentCellCoordinator做绑定和事件代理代码简洁可读性也会提高。

当页面足够复杂时如下图聊天界面

chatView.jpg

其主要功能如下:

解决问题的核心是按照功能等维度进行==拆分==,以低耦合的方式分散代码复杂度。将UI、交互、事件都放到各自子模块中处理。如下图:


ViewFramework.png

同时引入新的问题这些子模块怎么通信和交互?

对子模块的交互进行归类,主要有两种方式:

  1. 通知其他子模块,如:滚动tableview到底部等
  2. 子模块需要共享、监听数据的变动,如:多选状态、页面展示样式等

针对第一个问题,我们可以借鉴苹果系统的事件响应链的方式来解决多层传递的麻烦问题


responder.png
@protocol UIResponderEventProtocol
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo;
@end

@interface UIResponder (Router)
@property(nonatomic, weak) id<UIResponderEventProtocol> eventDelegate;
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo
@end

@implementation UIResponder (Router)
//沿着响应者链将事件往父视图传递, 事件最终被拦截处理 或者 无人处理直接丢弃
- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
    if([self.eventDelegate respondsToSelector:@selector(routeEvent:userInfo:)]) {
    
        [self.eventDelegate routeEvent:eventName userInfo:userInfo];
    }
    
    [self.nextResponder routeEvent:eventName userInfo:userInfo];
}
@end

针对第二种交互方式采用如下图所示,引入context(上下文)概念,保存UIViewController(控制器)、共享数据模型等。子模块可通过监听或获取context的数据模型来做相应的展示,通过UIViewController做吐司、push等操作。

context.png

代码例子:

3.1 context类

//聊天上下文,提供共享数据等
@interface ChatContext
@property(nonatomic, weak) UIViewController *controller;
@property(nonatomic, strong) BOOL selectModel; //编辑模式
@end

3.2 table协调器

//tableView的协调器,处理tableView和ViewModel的胶水代码
@interface TableViewCoordinator()
- (void)scrollToPositionWithMessageId:(NSNumber *)mid;
@end

3.2.1 tableCell协调器

//tableCell的协调器,处理tableCell和cellViewModel的胶水代码
@interface ChatCellCoordinator() <ChatCellDelegate>
@end
@implementation ChatCellCoordinator

- (void)onMenuForward:(ChatCell *)cell {
    SelectViewController *selectVC = [[SelectViewController alloc] init];
    [self.context.controller push:selectVC];
}

- (void)onMenuMultiSelect {
    self.context.selectModel = YES;
}
@end

3.3 输入框协调器

@implementation InputPanelViewCoordinator

- (void)bindData {
 [self.kvoController observe:self.context keyPath:@"selectModel" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionInitial block:^(id observer, id object, NSDictionary *change) {
        @strongify(self);
        [self.view.hidden = YES;
    }];
}
@end
  1. 聊天会话页面中的消息ChatCell长按会弹出菜单列表,选择转发按钮ChatCell的协调器ChatCellCoordinator可直接通过context的controller跳转到选人页面。
  2. 聊天会话页面中的消息ChatCell长按会弹出菜单列表,选择多选按钮进入编辑选择状态,协调器ChatCellCoordinator设置context的selectModel为YES切换为选择模式,输入面板子模块监听选择状态后隐藏同时显示跳转到啊选人界面的入口。聊天消息ChatCell也监听选择状态线束勾选按钮。

3.4 头部协调器

@interface HeaderViewCoordinator()<PromptViewProtocol>
@property(nonatomic, strong) PromptView *view;
@property(nonatomic, strong) NSNumber *unReadMaxMessageID;
@end

@implementation HeaderViewCoordinator 

- (void)onClickPromptView {
    //滚动到置顶位置
    NSDictory *dic = @{@"unReadMaxMessageID", self.unReadMaxMessageID};
    [self.view routeEvent:@"ScrollToPosition" userInfo:dic];
}
@end

3.5 ViewController

@interface ChatViewController ()<UIResponderEventProtocol>
@property(nonatomic, strong) ChatContext *context;
@property(nonatomic, strong) HeaderViewCoordinator *headerCoordinator;
@property(nonatomic, strong) TableViewCoordinator *tableViewCoordinator;
@property(nonatomic, strong) InputPanelViewCoordinator *inputPanelViewCoordinator;
@end

@implementation

- (void)viewDidLoad {

    self.view.eventDelegate = self;
    [self config];
}

- (void)routeEvent:(NSString *)eventName userInfo:(NSDictionary *)userInfo {
    NSNumber mid = [userInfo objectForKey:@"ScrollToPosition"];
    [self.tableViewCoordinator scrollToPositionWithMessageId:mid];
}

- (void)config {
    UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero];
    [self.view addSubView:tableView];
    //做布局
    self.tableViewCoordinator = [[TableViewCoordinator alloc] initWithTableView:tableView context:self.context];
    
    InputPanelView *inputView = [[InputPanelView alloc] initWithFrame:CGRectZero];
    [self.view addSubView:inputView];
    //做布局
    self.inputPanelViewCoordinator = [[InputPanelViewCoordinator alloc] initWithTableView:inputView context:self.context];
    
    //其他模块配置

}
@end

顶部未读消息弹框点击后由HeaderViewCoordinator代理执行点击事件,发现自己执行不了需要通知tableView子模块滚动。于是调用view的routeEvent: userInfo:事件路由通过视图的响应链到ChatViewController的view视图,ChatViewController拦截view的事件作为中介者调用tableViewCoordinator提供的滚动tableview的接口。于是完成功能。

通过上面的拆解职责分工清楚,减少代码臃肿提供代码的可读性和可维护性。

页面间数据流

如B页面设置了会话为免扰模式,A页面需要监听免扰模式从而做不同的视图展现。可以将免扰模式这个数据下沉到SDK中,然后开出免扰变动监听的接口。

Demo

[demo传送门]https://github.com/hubjf/AppFrameworkKit

FAQ

关于context传递问题

UIView分类头文件

@protocol ContextProtocol <NSObject>
@property(nonatomic, weak, readonly) UIViewController *controller;
- (instancetype)initWithViewController:(UIViewController *)controller;
@end

@interface UIView (Context)
@property(nonatomic, strong) id<ContextProtocol> context;
@end

UIView分类实现文件

static char *ContextProtocolKey = "ContextProtocolKey";
@implementation UIView (Context)
- (void)setContext:(id<ContextProtocol>)delegate {
    
    objc_setAssociatedObject(self, ContextProtocolKey, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (id<ContextProtocol>)context {
    id<ContextProtocol> context = [self p_context:self];
    if (!context) {
        context = [self parentContext:self.superview];
    }
    return context;
}

- (id<ContextProtocol>)parentContext:(UIView *)parentView {
    if (!parentView) {
        return nil;
    }
    id<ContextProtocol> context = [parentView p_context:parentView];
    if (!context) {
        context = [self parentContext:parentView.superview];
    } else {
        self.context = context;
    }
    
    return context;
}

- (id<ContextProtocol>)p_context:(id)object {
    id<ContextProtocol> context = objc_getAssociatedObject(object, ContextProtocolKey);

    return context;
}

创建业务的上下文ChatContext

@interface ChatContext : NSObject <ContextProtocol>
@property (nonatomic, assign, readonly) ChatBackGroundStyle bgStyle;
@end

然后在ViewController里面创建context并赋值

//ChatViewController.m文件
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.context = [[ChatContext alloc] initWithViewController:self];
    
    // Do any additional setup after loading the view.
    [self installSubModule];
    
    
}

使用者在Coordinator里面直接调用自己的View的context

//InputPanelCoordinator.m文件
- (void)bindData {
ChatContext *context = (ChatContext *)self.inputView.context;
}

关于context共享属性大家够随意修改问题

//ChatContext.h文件
@property (nonatomic, assign, readonly) ChatBackGroundStyle bgStyle;

//ChatContext.m文件
@interface ChatContext ()
{
    ChatBackGroundStyle chatContext_bgStyle;
}
@end

@implementation ChatContext
@synthesize bgStyle = chatContext_bgStyle;

- (void)setChatContext_bgStyle:(ChatBackGroundStyle)style {
    [self willChangeValueForKey:@"bgStyle"];
    chatContext_bgStyle = style;
    [self didChangeValueForKey:@"bgStyle"];
}
@end

推广使用

上述文章讲解了具体的思想和解决方案以及对应的代码实现,后续会提供工具类、架构模板,使用者只要创建文件的时候根据场景选择对应的架构就能够帮你创建整个架构文件,然后只需要在里面写业务逻辑即可。

设计思想

上一篇 下一篇

猜你喜欢

热点阅读