基于MVVM,用于快速搭建设置页,个人信息页的框架

更新记录:

2017.4.23:新增支持数据源完全依赖网络请求的情况。
2017.4.22:新增支持请求新数据后刷新表格。
2017.4.21: 新增CocoaPods支持:pod ‘SJStaticTableView’, ‘~> 1.2.0’。


写一个小小轮子~

写UITableView的时候,我们经常遇到的是完全依赖于网络请求,需要自定义的动态cell的需求(比如微博帖子列表)。但是同时,大多数app里面几乎也都有设置页,个人页等其他以静态表格为主的页面。

而且这些页面的共性比较多:
1. 大多数情况下在进入页面之前就已经拿到所有数据。
2. cell样式单一,自定义cell出现的几率比较小(几乎都是高度为44的cell)。
3. 多数都分组。

因为自己非常想写一个开源的东西出来(也可以暴露自己的不足),同时又受限于水平,所以就打算写这么一个比较简单,又具有通用性的框架:一个定制性比较高的适合在个人页和设置页使用的UITableView

在真正写之前,看了几篇类似的文章,挑出三篇自己觉得比较好的:
1. Clean Table View Code
2. 如何写好一个UITableView
3. 利用MVVM设计快速开发个人中心、设置等模块

看完总结之后,利用上周3天的业余时间写好了这个框架,为了它实用性,我仿照了微信客户端的发现页,个人页和设置页写了一个Demo,来看一下效果图:

发现页 | 个人页 | 个人信息页 | 设置页

项目所用资源来自:GitHub:zhengwenming/WeChat
Demo地址:GitHub: knightsj/SJStaticTableView

为了体现出这个框架的定制性,我自己也在里面添加了两个页面,入口在设置页里面:

分组定制 | 同组定制

先不要纠结分组定制和同组定制的具体意思,在后面讲到定制性的时候我会详细说明。现在只是让大家看一下效果。

在大概了解了功能之后,开始详细介绍这个框架。写这篇介绍的原因倒不是希望有多少人来用,而是表达一下我自己的思路而已。各位觉得不好的地方请多批评。

在正式讲解之前,先介绍一下本篇的基本目录:
1. 用到的技术点。
2. 功能说明。
3. 使用方法。
4. 定制性介绍。
5. 新增支持刷新功能。
6. 新增支持数据源完全依赖网络请求。

1. 用到的技术点


框架整体来说还是比较简单的,主要还是基于苹果的UITableView组件,为了解耦和责任分离,主要运用了以下技术点:
- MVVM:采用MVVM架构,将每一行“纯粹”的数据交给一个单独的ViewModel,让其持有每个cell的数据(行高,cell类型,文本宽度,图片高度等等)。而且每一个section也对应一个ViewModel,它持有当前section的配置数据(title,header和footer的高度等等)。
- 轻UIViewController:分离UITableViewDataSourceUIViewController,让单独一个类来实现UITableViewDataSource的职能。
- block:使用block来调用cell的绘制方法。
- 分类:使用分类来定义每一种不同的cell的绘制方法。

知道了主要运用的技术点以后,给大家详细介绍一下该框架的功能。

2. 功能介绍


这个框架可以用来快速搭建设置页,个人信息页能静态表格页面,使用者只需要给tableView的DataSource传入元素是viewModel的数组就可以了。

虽说这类页面的布局还是比较单一的,但是还是会有几种不同的情况(cell的布局类型),我对比较常见的cell布局做了封装,使用者可以直接使用。

我在定义这些cell的类型的时候,大致划分了两类:
1. 第一类是系统风格的cell,大多数情况下,cell高度为44;在cell左侧会有一张图,一个label,也可以只存在一种(但是只存在图片的情况很少);在cell右侧一般都有一个向右的箭头,而且有时这个箭头的左侧还可能有label,image,也可以两个都有。
2. 第二类就是自定义的cell了,它的高度不一定是44,而且布局和系统风格的cell很不一样,需要用户自己添加。

基于这两大类,再细分了几种情况,可以由下面这张图来直观看一下:

既然是cell的类型,那么就类型的枚举就需要定义在cell的viewModel里面:

typedef NS_ENUM(NSInteger, SJStaticCellType) {

    //系统风格的各种cell类型,已封装好,可以直接用
    SJStaticCellTypeSystemLogout,                          //退出登录cell
    SJStaticCellTypeSystemAccessoryNone,                   //右侧没有任何控件
    SJStaticCellTypeSystemAccessorySwitch,                 //右侧是开关
    SJStaticCellTypeSystemAccessoryDisclosureIndicator,    //右侧是三角箭头(箭头左侧可以有一个image或者一个label,或者二者都有,根据传入的参数决定)

    //需要用户自己添加的自定义cell类型
    SJStaticCellTypeMeAvatar,                              //个人页“我”cell    
};

来一张图直观得体会一下:

支持cell类型

在这里有三点需要说一下:

  1. 这里面除了自定义的cell以外,其他类型的cell都不需要开发者自己布局,都已经被我封装好,只需要在cell的ViewModel里面传入相应的类型和数据(文字,图片)即可。
  2. 因为左侧的两个控件(图片和文字)是至少存在一个而且左右顺序固定(图片永远在最左侧),所以该框架通过开发者传入的左侧需要显示的图片和文字,可以自己进行cell的布局。所以类型的判断主要作用于cell的右侧。
  3. 值得一提的是,在”最右侧是一个箭头”子分支的五个类型其实都属于一个类型,只需要传入文字和图片,以及文字图片的显示顺序参数(这个参数只在同时存在图片和文字的时候有效)就可以自行判断布局。

在了解了该框架的功能之后,我们先看一下如何使用这个框架:

3. 使用方法


集成方法:

  1. 静态:手动将SJStaticTableViewComponent文件夹拖入到工程中。
  2. 动态:CocoaPods:pod 'SJStaticTableView', '~> 1.1.2

具体的方法先用文字说明一下:

  1. 将要开发的页面的ViewController继承SJStaticTableViewController
  2. 在新ViewController里实现createDataSource方法,将viewModel数组传给控制器的dataSource属性。
  3. 根据不同的cell类型,调用不同的cell绘制方法。
  4. 如果需要接受cell的点击,需要实现didSelectViewModel方法。

可能感觉比较抽象,我拿设置页来具体说明一下:

先看一下设置页的布局:

设置页

然后我们看一下设置的ViewController的代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"设置";
}


- (void)createDataSource
{
    self.dataSource = [[SJStaticTableViewDataSource alloc] initWithViewModelsArray:[Factory settingPageData] configureBlock:^(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel *viewModel) {

        switch (viewModel.staticCellType)
        {
            case SJStaticCellTypeSystemAccessoryDisclosureIndicator:
            {
                [cell configureAccessoryDisclosureIndicatorCellWithViewModel:viewModel];
            }
                break;

            case SJStaticCellTypeSystemAccessorySwitch:
            {
                [cell configureAccessorySwitchCellWithViewModel:viewModel];
            }
                break;

            case SJStaticCellTypeSystemLogout:
            {
                [cell configureLogoutTableViewCellWithViewModel:viewModel];
            }
                break;

            case SJStaticCellTypeSystemAccessoryNone:
            {
                [cell configureAccessoryNoneCellWithViewModel:viewModel];
            }
                break;

            default:
                break;
        }
    }];
}


- (void)didSelectViewModel:(SJStaticTableviewCellViewModel *)viewModel atIndexPath:(NSIndexPath *)indexPath
{

    switch (viewModel.identifier)
    {

        case 6:
        {
            NSLog(@"退出登录");
            [self showAlertWithMessage:@"真的要退出登录嘛?"];
        }
            break;

        case 8:
        {
            NSLog(@"清理缓存");
        }
            break;

        case 9:
        {
            NSLog(@"跳转到定制性cell展示页面 - 分组");
            SJCustomCellsViewController *vc = [[SJCustomCellsViewController alloc] init];
            [self.navigationController pushViewController:vc animated:YES];
        }
            break;

        case 10:
        {
            NSLog(@"跳转到定制性cell展示页面 - 同组");
            SJCustomCellsOneSectionViewController *vc = [[SJCustomCellsOneSectionViewController alloc] init];
            [self.navigationController pushViewController:vc animated:YES];
        }
            break;

        default:
            break;
    }
}

看到这里,你可能会有这些疑问:
1. UITableViewDataSource方法哪儿去了?
2. viewModel数组是如何设置的?
3. cell的绘制方法是如何区分的?
4. UITableViewDelegate的方法哪里去了?

下面我会一一解答,看完了下面的解答,就能几乎完全掌握这个框架的思路了:

问题1:UITableViewDataSource方法哪儿去了?

我自己封装了一个类SJStaticTableViewDataSource专门作为数据源,需要控制器给它一个viewModel数组。

来看一下它的实现文件:

//SJStaticTableViewDataSource.m
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return self.viewModelsArray.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.cellViewModelsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //获取section的ViewModel
    SJStaticTableviewSectionViewModel *sectionViewModel = self.viewModelsArray[indexPath.section];
    //获取cell的viewModel
    SJStaticTableviewCellViewModel *cellViewModel = sectionViewModel.cellViewModelsArray[indexPath.row];

    SJStaticTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellViewModel.cellID];
    if (!cell) {
        cell = [[SJStaticTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellViewModel.cellID];
    }
    self.cellConfigureBlock(cell,cellViewModel);

    return cell;

}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.sectionHeaderTitle;  
}

- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.sectionFooterTitle;
}

表格的cell和section都设置了与其对应的viewModel,用于封装其对应的数据:

cell的viewModel(大致看一下即可,后面有详细说明):

typedef NS_ENUM(NSInteger, SJStaticCellType) {

    //系统风格的各种cell类型,已封装好,可以直接用
    SJStaticCellTypeSystemLogout,                          //退出登录cell(已封装好)
    SJStaticCellTypeSystemAccessoryNone,                   //右侧没有任何控件
    SJStaticCellTypeSystemAccessorySwitch,                 //右侧是开关
    SJStaticCellTypeSystemAccessoryDisclosureIndicator,    //右侧是三角箭头(箭头左侧可以有一个image或者一个label,或者二者都有,根据传入的参数决定)

    //需要用户自己添加的自定义cell类型
    SJStaticCellTypeMeAvatar,                              //个人页“我”cell

};


typedef void(^SwitchValueChagedBlock)(BOOL isOn);           //switch开关切换时调用的block


@interface SJStaticTableviewCellViewModel : NSObject

@property (nonatomic, assign) SJStaticCellType staticCellType;                  //类型


@property (nonatomic, copy)   NSString *cellID;                                  //cell reuser identifier
@property (
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值