美文网首页iOS学习iOS开发程序员
iOS 客户端编程框架的学习

iOS 客户端编程框架的学习

作者: TotoroLee | 来源:发表于2016-12-06 18:40 被阅读111次

    本文介绍 MVVM 和 MVVM without Binding with DataController 这两种编程框架,主要以代码方式展现。发展历史和概念性问题可自行�查找哈。

    MVVM

    推荐阅读 被误解的 MVC 和被神化的 MVVM
    Model-View-ViewModel
    MVVM是为了实现Model层与View层的解耦。将ViewModel作为VM的�中介,在Model发生改变的时候,View也会自动变化。使用双向绑定技术,通常使用RAC(ReactiveCocoa)这个框架来完成。

    该�例子主要使用RAC的方法是 RACObserve(TARGET, KEYPATH) 。TARGET是监听目标,KEYPATH是要观察的属性值,当属性值发生变化的时候会调用 subscribeNext:(void (^)(id x))nextBlock 这个方法,并将KEYPATH的最新属性值通过x传递。

    实现目的:共有三个控制器,商品分类列表 SortListViewController --> 商品列表 GoodsListViewController --> 商品详情页 GoodsDetailViewController。功能为 商品分类列表 将分类ID传入 商品列表界面,使用分类ID请求该分类下的数据在 商品列表页面展示,点击商品列表将商品ID传入商品详情页。

    �流程图.png
    SortListViewController
    GoodsListModel *model = [[GoodsListModel alloc]init];
    model.sortID = @"分类ID";
    GoodsListViewModel *viewModel = [[GoodsListViewModel alloc]initWithGoodsListModel:model];
        
    GoodsListViewController *goodsListVC = [[GoodsListViewController alloc]init];
    goodsListVC.viewModel = viewModel;
    [self.navigationController pushViewController:goodsListVC animated:YES];
    

    此处的push传参是改变model.sortID 使GoodsListViewModel中的sortID监听发生改变,并保证每次传入不同的sortID都会执行方法。之后通过initWithGoodsListModel: 这个方法让GoodsListViewModel 进行数据请求,从而给ViewModel中的viewModelArray赋值,并将GoodsListViewModel与GoodsListViewController控制器绑定。

    GoodsListViewController

    商品列表这个控制器展现的为一个UICollectionView ,使用MVVM的框架修改,具体分为
    GoodsListModel
    GoodsListViewController
    GoodsListViewModel
    GoodsListView
    GoodsListCellViewModel
    GoodsListCell

    • GoodsListModel
    GoodsListViewModel.h
    #import <Foundation/Foundation.h>
    @interface GoodsListModel : NSObject
    @property (strong ,nonatomic) NSString *sortID;             //分类ID
    @property (strong ,nonatomic) NSString *goodsID;            //商品ID
    @property (strong ,nonatomic) NSString *goodsName;          //商品名称
    @property (strong ,nonatomic) NSString *goodsPrice;         //商品价格
    @end
    

    这里解释下sortID, 商品列表GoodsListViewController这个控制器只是需要sortID来进行数据请求,并不要把这个sortID写入该控制器的Model类中。之所以写入,是便于每次传入sortID 而进行的数据请求,仅仅是为了方便起见,在GoodsListViewModel中网络请求完成的字典转模型时并不需要该参数。
    也可以使用商品分类列表SortListViewController控制器的model类,在传入GoodsListViewModel 和 GoodsListViewModel 监听的model 做出修改即可。

    • GoodsListViewController
    GoodsListViewController.h 
    #import <UIKit/UIKit.h>
    @class GoodsListViewModel;
    @interface GoodsListViewController : UIViewController
    @property (strong ,nonatomic) GoodsListViewModel *viewModel;
    @end
    
    GoodsListViewController.m
    #import "GoodsListViewController.h"
    #import "GoodsListViewModel.h"
    #import "GoodsListView.h"
    #import "GoodsDetailViewController.h"
    @interface GoodsListViewController ()
    @property (strong ,nonatomic) GoodsListView *goodsListView;
    @end
    
    @implementation GoodsListViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        [self.view addSubview:self.goodsListView]
        [RACObserve(self.viewModel, viewModelArray) subscribeNext:^(id x) {
            self.goodsListView.viewModel = self.viewModel;
        }];
       
        [[self.viewModel.cellClickSubject takeUntil:self.rac_willDeallocSignal]subscribeNext:^(id x) {
            GoodsDetailViewController *goodsDetailsVC = [[GoodsDetailViewController alloc]init];
            goodsDetailsVC.goodsID = x;
            [self.navigationController pushViewController:goodsDetailsVC animated:YES];
        }];
    }
    
    - (GoodsListView *)goodsListView{
        if (!_goodsListView) {
            _goodsListView = [[GoodsListView alloc]initWithFrame:CGRectMake(0, 0, kScreenW, kScreenH)];
        }
        return _goodsListView;
    }
    @end
    

    商品列表控制器,在上个控制器push进来的时候已经将GoodsListViewModel与GoodsListViewController绑定。
    RACObserve这个方法是监听GoodsListViewModel 中的viewModelArray (viewModelArray可以看做是网络请求字典转模型之后的模型数组)。当viewModelArray改变时将新的ViewModel传递给GoodsListView。实现了GoodsListViewController 与 goodsListView的绑定。这样双向绑定就完成了。
    goodsListView 可以看做子视图的集合View,控制器仅仅需要在数据发生改变的时候告诉goodsListView即可,不需要管理数据的内容和数据的展示方式。

    • GoodsListViewModel
    GoodsListViewModel.h
    #import <Foundation/Foundation.h>
    @class GoodsListCellViewModel;
    @class GoodsListModel;
    @interface GoodsListViewModel : NSObject
    @property (copy, nonatomic) NSArray * viewModelArray;
    @property (nonatomic, strong) RACSubject *cellClickSubject;
    - (instancetype)initWithGoodsListModel: (GoodsListModel *) model;
    - (void)first;
    - (void)next;
    - (GoodsListCellViewModel *)itemViewModelForIndex:(NSInteger)index;
    @end
    
    GoodsListViewModel.m
    #import "GoodsListViewModel.h"
    #import "GoodsListModel.h"
    #import "GoodsListCellViewModel.h"
    @interface GoodsListViewModel ()
    @property (strong ,nonatomic) NSString *sortID;
    @property (strong, nonatomic) NSNumber  *nextPageNumber;
    @property (strong, nonatomic)NSDictionary *paramsDic;
    @end
    
    @implementation GoodsListViewModel
    - (instancetype)initWithGoodsListModel:(GoodsListModel *)model{
        self = [super init];
        if (nil != self) {
            [RACObserve(model, sortID) subscribeNext:^(NSString *sort) {
                self.sortID = sort;
                self.nextPageNumber = @1;
            }];
            [self requestInfo];
        }
        return self;
    }
    
    - (void)requestInfo{
        [RACObserve(self, nextPageNumber)  subscribeNext:^(NSNumber * nextPageNumber) {
            NSDictionary *params = @{@"id":self.sortID,
                                     @"page":nextPageNumber,
                                     @"num":@15,};
            self.paramsDic = params;
        }];
     
        [[RACObserve(self, paramsDic) filter:^BOOL(id value) {
            return value;
        }] subscribeNext:^(NSDictionary * params) {
            //网络请求并给_viewModelArray赋值
            
            //RAC 的网络请求
            [[self.httpClient rac_POST:@"URL" parameters:params] subscribeNext:^(RACTuple *JSONAndHeaders) {
                JSONAndHeaders.first  为请求内容
            }];
        }];
    }
    
    - (void)first{
        self.nextPageNumber = @1;
    }
    
    - (void)next{
        self.nextPageNumber = [NSNumber numberWithInteger: [self.nextPageNumber integerValue] + 1];
    }
    
    - (GoodsListCellViewModel *)itemViewModelForIndex:(NSInteger)index{
        return [[GoodsListCellViewModel alloc]initWithModel:[_viewModelArray objectAtIndex:index]];
    }
    
    - (RACSubject *)cellClickSubject {
        if (!_cellClickSubject) {
            _cellClickSubject = [RACSubject subject];
        }
        return _cellClickSubject;
    }
    
    @end
    

    ViewModel核心代码。可以看出nextPageNumber页码参数,paramsDic都是被观察的对象,每当新的sortID传入时,nextPageNumber都会重置为1,从而影响paramsDic,进而走网络请求。
    initWithModel 方法将模型数组_viewModelArray 转为 GoodsListCellViewModel中的参数。
    itemViewModelForIndex 方法在GoodsListView中给cell绑定数据的时候调用。
    first 方法用于下拉刷新,将nextPageNumber重置为1。
    next 方法用于上拉加载,将nextPageNumber +1。

    • GoodsListView
    GoodsListView.h
    #import <UIKit/UIKit.h>
    @class GoodsListViewModel;
    @interface GoodsListView : UIView
    @property (nonatomic, strong) GoodsListViewModel *viewModel;
    - (instancetype)initWithFrame:(CGRect)frame;
    @end
    
    GoodsListView.m
    #import "GoodsListView.h"
    #import "GoodsListViewModel.h"
    #import "GoodsListCell.h"
    @interface GoodsListView ()
    @property (strong ,nonatomic) UICollectionView *collectionView;
    @end
    
    @implementation GoodsListView
    - (instancetype)initWithFrame:(CGRect)frame{
        self = [super initWithFrame:frame];
        if (self) {
            [self setupView];
            [RACObserve(self, self.viewModel) subscribeNext:^(id x) {
                [_collectionView reloadData];
            }];
        }
        return self;
    }
    
    - (void)setupView{
        //_collectionView 添加
        _collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
            [self.viewModel first];
        }];
        _collectionView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
            [self.viewModel next];
        }];
    }
    //此处省去�UICollectionView的代理
    
    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
        GoodsListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellIdentifier" forIndexPath:indexPath];
        cell.cellViewModel = [self.viewModel itemViewModelForIndex:indexPath.row];
        return cell;
    }
    -(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
        [self.viewModel.cellClickSubject sendNext:@"点击商品的ID"];
    }
    @end
    

    子视图的集合,继承于UIView。这里仅放置了一个UICollectionView,添加了上拉加载,下拉刷新的功能,省去了_collectionView的创建和代理的设置。在初始化的时候将self.viewModel作为被观察的对象,当商品列表控制器传递过来的viewModel改变时将会调用UICollectionView的刷新方法。
    itemViewModelForIndex:indexPath 这个方法是给cell绑定cellViewModel的数据,其实这里也可以省略,直接将viewModel的viewModelArray直接传给cell,省去了GoodsListCellViewModel的创建。

    • GoodsListCellViewModel
    GoodsListCellViewModel.h
    #import <Foundation/Foundation.h>
    @class GoodsListModel;
    @interface GoodsListCellViewModel : NSObject
    - (instancetype)initWithModel:(GoodsListModel *)model;
    @property (strong ,nonatomic) NSString *goodsID;
    @property (strong ,nonatomic) NSString *goodsName;
    @property (strong ,nonatomic) NSString *goodsPrice;
    @end
    
    GoodsListCellViewModel.m
    #import "GoodsListCellViewModel.h"
    #import "GoodsListModel.h"
    @interface GoodsListCellViewModel ()
    @property (strong ,nonatomic) GoodsListModel *model;
    @end
    
    @implementation GoodsListCellViewModel
    - (instancetype)initWithModel:(GoodsListModel *)model{
        self = [super init];
        if (self) {
            self.model = model;
        }
        return self;
    }
    - (NSString *)goodsID{
        return self.model.goodsID;
    }
    - (NSString *)goodsName{
        return self.model.goodsName;
    }
    - (NSString *)goodsPrice{
        return self.model.goodsPrice;
    }
    @end
    

    将viewModel中viewModelArray的模型转为 GoodsListCellViewModel

    • GoodsListCell
    GoodsListCell.h
    #import <UIKit/UIKit.h>
    @class GoodsListCellViewModel;
    @interface GoodsListCell : UICollectionViewCell
    @property (strong ,nonatomic) GoodsListCellViewModel *cellViewModel;
    @end
    
    GoodsListCell.m
    #import "GoodsListCell.h"
    #import "GoodsListCellViewModel.h"
    @implementation GoodsListCell
    //Cell的布局
    - (void)setCellViewModel:(GoodsListCellViewModel *)cellViewModel{
        _cellViewModel = cellViewModel;
        //cell控件的赋值
    }
    @end
    

    使用GoodsListCellViewModel进行赋值即可

    MVVM without Binding with DataController

    参考 猿题库 iOS 客户端架构设计

    �图片引自<猿题库 iOS 客户端架构设计>.png

    控制器通过 DataController 请求数据,并将数据装配给ViewModel,ViewModel将数据中的模型转为View 对应的ViewModel ,View负责展示UI,并将事件传递给控制器。

    本例子为商品列表页,默认子视图为CollectionView。分为
    GoodsListViewController
    GoodsListModel
    GoodsListDataController
    GoodsListViewModel
    GoodsListCellViewModel
    GoodsListView
    GoodsListCell

    • GoodsListViewController
    GoodsListViewController.m
    #import "GoodsListViewController.h"
    #import "GoodsListDataController.h"
    #import "GoodsListView.h"
    #import "GoodsListViewModel.h"
    
    @interface GoodsListViewController ()<GoodsListViewDelegate>
    @property (strong ,nonatomic) GoodsListDataController *dataManager;
    @property (strong ,nonatomic) GoodsListView *goodsListView;
    @end
    
    @implementation GoodsListViewController
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.navigationItem.title = @"商品列表";
        [self.view addSubview:self.goodsListView];
        [self fectchGoodsModelData];
    }
    
    - (void)fectchGoodsModelData{
        [self.dataManager requestGoodsModelDataWhenSucess:^{
            if (self.dataManager.goodsListModelArray.count > 0) {
                [self renderGoodsListView];
            }
        } fail:^(NSError *error) {
        }];
    }
    
    - (void)renderGoodsListView{
        GoodsListViewModel *viewModel = [GoodsListViewModel viewModelWithGoodsModel:self.dataManager.goodsListModelArray];
        [self.goodsListView bindDataWithViewModel:viewModel];
    }
    
    - (void)didSelectedRowOfGoodsID:(NSString *)goodsID{
        //子视图传递事件的代理
    }
    
    - (GoodsListDataController *)dataManager{
        if (!_dataManager) {
            _dataManager = [[GoodsListDataController alloc]init];
        }
        return _dataManager;
    }
    - (GoodsListView *)goodsListView{
        if (!_goodsListView) {
            _goodsListView = [[GoodsListView alloc]initWithFrame:CGRectMake(0, 0, kScreenW, kScreenH)];
            _goodsListView.delegate = self;
        }
        return _goodsListView;
    }
    @end
    

    商品列表控制器。添加子视图集合GoodsListView,初始化GoodsListDataController。
    fectchGoodsModelData 方法使dataManager请求�网络请求,成功之后调用 renderGoodsListView 方法将数据装配到 ViewModel 中,再将ViewModel传递至 GoodsListView,更新数据刷新UI。 GoodsListView 中的事件通过代理传递到控制器中。

    • GoodsListModel
    GoodsListModel.h
    #import <Foundation/Foundation.h>
    @interface GoodsListModel : NSObject
    @property (strong ,nonatomic) NSString *goodsID;
    @property (strong ,nonatomic) NSString *goodsName;
    @property (strong ,nonatomic) NSString *goodsPrice;
    @end
    

    简单的商品属性

    • GoodsListDataController
    GoodsListDataController.h
    #import <Foundation/Foundation.h>
    @class GoodsListModel;
    @interface GoodsListDataController : NSObject
    @property (strong ,nonatomic) NSMutableArray<GoodsListModel *> *goodsListModelArray;
    - (void)requestGoodsModelDataWhenSucess:(void (^)())sucess fail:(void(^)(NSError *))fail;
    @end
    
    GoodsListDataController.m
    #import "GoodsListDataController.h"
    #import "GoodsListModel.h"
    @implementation GoodsListDataController
    - (instancetype)init{
        self = [super init];
        if (self) {
            self.goodsListModelArray = [[NSMutableArray alloc]init];
        }
        return self;
    }
    
    - (void)requestGoodsModelDataWhenSucess:(void (^)())sucess fail:(void (^)(NSError *))fail{
        self.goodsListModelArray = 模型数组
    }
    @end
    
    

    DataController负责数据请求,并将模型数组赋值给self.goodsListModelArray。DataController数据的获取和处理从ViewModel抽离,一定程度上简化了ViewModel的工作量,当DataController收到外界ViewController的请求后进行数据处理,进而更新UI。这样ViewController不需要关心数据的获取和处理,DataController不需要关心数据的展示和交互。

    • GoodsListViewModel
    GoodsListViewModel.h
    #import <Foundation/Foundation.h>
    @class GoodsListModel;
    @class GoodsListCellViewModel;
    @interface GoodsListViewModel : NSObject
    @property (strong ,nonatomic) NSArray <GoodsListCellViewModel *> *cellViewModelArray;
    + (GoodsListViewModel *)viewModelWithGoodsModel:(NSArray <GoodsListModel *>*)goodsListModelArray;
    @end
    
    GoodsListViewModel.m
    #import "GoodsListViewModel.h"
    #import "GoodsListModel.h"
    #import "GoodsListCellViewModel.h"
    @implementation GoodsListViewModel
    + (GoodsListViewModel *)viewModelWithGoodsModel:(NSArray <GoodsListModel *>*)goodsListModelArray{
        GoodsListViewModel *viewModel = [[GoodsListViewModel alloc]init];
        NSMutableArray *cellViewModelArray = [[NSMutableArray alloc]init];
        
        for (GoodsListModel *goodsListModel in goodsListModelArray) {
            GoodsListCellViewModel *cellViewModel = [GoodsListCellViewModel viewModelWithGoodsListModel:goodsListModel];
            [cellViewModelArray addObject:cellViewModel];
        }
        
        viewModel.cellViewModelArray = cellViewModelArray;
        return viewModel;
    }
    @end
    

    ViewModel,将数据装配到viewModel。此处使用工厂方法创建一个带有cellViewModelArray属性的GoodsListViewModel对象。具体是将网络请求之后的goodsListModelArray模型数组转为 GoodsListCellViewModel类型的数组,并将这种类型的数组作为ViewModel的一个属性。

    • GoodsListCellViewModel
    GoodsListCellViewModel.h
    #import <Foundation/Foundation.h>
    @class GoodsListModel;
    @interface GoodsListCellViewModel : NSObject
    @property (strong ,nonatomic) NSString *goodsID;
    @property (strong ,nonatomic) NSString *goodsName;
    @property (strong ,nonatomic) NSString *goodsPrice;
    + (GoodsListCellViewModel *)viewModelWithGoodsListModel:(GoodsListModel *)goodsListModel;
    @end
    
    GoodsListCellViewModel.m
    #import "GoodsListCellViewModel.h"
    #import "GoodsListModel.h"
    @implementation GoodsListCellViewModel
    + (GoodsListCellViewModel *)viewModelWithGoodsListModel:(GoodsListModel *)goodsListModel{
        GoodsListCellViewModel *cellViewModel = [[GoodsListCellViewModel alloc]init];
        cellViewModel.goodsID = goodsListModel.goodsID;
        cellViewModel.goodsName = goodsListModel.goodsName;
        cellViewModel.goodsPrice = goodsListModel.goodsPrice;
        return cellViewModel;
    }
    
    @end
    

    将viewModel中goodsListModelArray的模型转为 GoodsListCellViewModel

    • GoodsListView
    GoodsListView.h
    #import <UIKit/UIKit.h>
    @class GoodsListViewModel;
    @protocol GoodsListViewDelegate <NSObject>
    - (void)didSelectedRowOfGoodsID:(NSString *)goodsID;
    @end
    @interface GoodsListView : UIView
    @property (weak, nonatomic) id<GoodsListViewDelegate> delegate;
    - (void)bindDataWithViewModel:(GoodsListViewModel *)viewModel;
    @end
    
    GoodsListView.m
    #import "GoodsListView.h"
    #import "GoodsListViewModel.h"
    #import "GoodsListCell.h"
    #import "GoodsListCellViewModel.h"
    @interface GoodsListView ()
    @property (strong ,nonatomic) GoodsListViewModel *viewModel;
    @property (strong ,nonatomic) UICollectionView *collectionView;
    @end
    @implementation GoodsListView
    - (instancetype)initWithFrame:(CGRect)frame{
        self = [super initWithFrame:frame];
        if (self) {
            //子视图 初始化,布局
        }
        return self;
    }
    - (void)bindDataWithViewModel:(GoodsListViewModel *)viewModel{
        self.viewModel = viewModel;
        [self.collectionView reloadData];
    }
    - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
        return self.viewModel.cellViewModelArray.count;
    }
    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
        GoodsListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"GoodsListCell" forIndexPath:indexPath];
        GoodsListCellViewModel *cellViewModel = self.viewModel.cellViewModelArray[indexPath.row];
        [cell bindGoodsListCellDataWithCellViewModel:cellViewModel];
        return cell;
    }
    - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
        GoodsListCellViewModel *cellViewModel = self.viewModel.cellViewModelArray[indexPath.row];
        if ([self.delegate respondsToSelector:@selector(didSelectedRowOfGoodsID:)]) {
            [self.delegate didSelectedRowOfGoodsID:cellViewModel.goodsID];
        }
    }
    @end
    

    GoodsListView,子视图的集合。当数据装配至ViewModel时,通过bindDataWithViewModel: 方法将带有cellViewModelArray属性的ViewModel传递至子视图,使子视图刷新UI。bindGoodsListCellDataWithCellViewModel: 方法为Cell绑定数据,collectionView的点击方法通过代理传递给控制器。

    • GoodsListCell
    GoodsListCell.h
    #import <UIKit/UIKit.h>
    @class GoodsListCellViewModel;
    @interface GoodsListCell : UICollectionViewCell
    - (void)bindGoodsListCellDataWithCellViewModel:(GoodsListCellViewModel *)cellViewModel;
    @end
    
    GoodsListCell.m
    #import "GoodsListCell.h"
    #import "GoodsListCellViewModel.h"
    @implementation GoodsListCell
    - (void)bindGoodsListCellDataWithCellViewModel:(GoodsListCellViewModel *)cellViewModel{
        //控件赋值
    }
    @end
    
    

    将单个的cellViewModel值传递到Cell,并给Cell中的控件赋值。

    结语

    MVVM 使用的是双向绑定技术,需要RAC的支持,学习成本较大。
    MVVM without Binding with DataController 是通过数据的装配和代理的方法实现了解耦。�并且将ViewModel中的网络请求给单独�抽离出来作为DataController,使ViewModel的结构更加简洁。理解较为容易。
    MVC 简单经典的�架构,如果把基类写的足够便利,简单的页面还是推荐使用。
    需要依据项目的开发和维护来选择合适的架构,有利有弊。

    这是几个月前技术分享的内容,PPT上显示的是16/6/28,貌似好久了。当时用项目中的代码,把大家讲的可晕。现在把思路重写理了一遍,发现好多了,就记录下来,以后看着也方便。

    相关文章

      网友评论

        本文标题:iOS 客户端编程框架的学习

        本文链接:https://www.haomeiwen.com/subject/jczlmttx.html