Skip to content

A data-driven UICollectionView&UITableView framework for building fast and flexible lists by declarative syntax.

License

Notifications You must be signed in to change notification settings

fangyuxi/WBListKit

Folders and files

NameName
Last commit message
Last commit date

Latest commit

author
fyx.cpp
Apr 15, 2019
9d9e77b · Apr 15, 2019
Apr 15, 2019
Feb 26, 2019
Nov 2, 2018
Mar 29, 2017
Mar 29, 2017
Apr 1, 2019
Apr 1, 2019
Feb 25, 2019
Mar 29, 2017

Repository files navigation

WBListKit

996.icu LICENSE

简介

列表,在iOS中是最常用的UI,可以说我们在开发项目的时候有一半以上的时间是在和列表打交道,但是在写列表的时候我们并不愉快
为了解决在项目中使用UITableView和UICollectionView的时候,要写很多重复的代码,而且每个人写法和代码风格不统一的问题,WBListKit封装了UITableView和UICollectionView。

特性

  • 简单易用,会让团队中每个成员的代码风格统一,容易维护
  • 功能强大,几乎适用于所有列表样式的UI
  • 无类型污染,框架是基于协议设计,不存在继承体系,没有强耦合,扩展性强
  • drop in clean,零成本接入现有项目,对旧代码没有影响
  • 对下拉刷新和上拉加载更多有很好的支持
  • 对空页面和错误页面提示提供了插件化支持
  • 集成了自动differ功能,再也不用手动调用insertRow,deleteRow,只刷新变化的部分
  • 支持Swift混编

系统要求

  • iOS 7.0 以上系统
  • Xcode 7.3 或更高版本

Author

[email protected], [email protected]

License

WBListKit is available under the MIT license. See the LICENSE file for more info.

使用教程

注意: 项目中有比较完善的Demo,可以查看

设计思路

注意: 只介绍针对UITableView的实现方式,UICollectionView的实现大体相似,后续只介绍不同的地方

针对UITableView的每一行,抽象成对象WBTableRow,针对UITableView的每一个section,抽象成WBTableSection对象,将Header和Footer抽象成WBTableSectionHeaderFooter对象
同时提供了WBTableSectionMaker 用于配置section并实现链式操作
WBTableViewAdapter实现了UITableView的全部数据源方法和部分代理方法,将之前重复的(团队中成员为了实现相同的逻辑,但是代码并不相同)代码封装成一个内聚的对象
WBTableViewAdapter并不关心数据从哪里来(Tag1),只是将数据按照一定的格式拼装(Tag2)给UITableView使用,下面分别介绍下这几个对象

WBTableRow

WBTableRow 代表了一行,同时充当了UITableViewCell的模型,主要完成以下工作

  • 通过属性associatedCellClass 关联一个已经实现了WBTableCellProtocol 协议的UITableViewCell对象(同时支持NIB)
  • 可以配置这个Cell是使用自动布局的方式确定高度还是使用Frame方式
  • 框架除了会帮你通过IndexPath确定Cell的具体位置之外,还会根据具体位置抽象出一个WBTableRowPosition,帮你确定Cell具体是Top,Bottom,Middle,Single,这样对于一些要根据Cell具体位置布局UI的情况就很方便了很多
  • 提供一个data属性,为Cell提供真正的数据源,data对象到底是什么类型,下面再讨论(这样的好处就在于不用为每一种类型的Cell都创建一种Row类型)

WBTableSection

  • 装配section,增删改查Row
  • section提供id,逻辑层就不用关心section的具体位置,只要通过id就可以找到想要的section,能够解决一部分UI位置一变,逻辑层也跟着变的窘境,一定程度也会解决各种数组越界的问题
  • section添加footerheader
  • section 支持diff操作

WBTableSectionHeaderFooter

  • WBTableSectionHeaderFooter类似于WBTableRow,都是给数据驱动的View(Tag3)提供模型
  • 通过属性associatedHeaderFooterClass关联一个已经实现了WBTableHeaderFooterViewProtocal协议的UITableViewReusebleView对象(同时支持NIB)
  • 可以配置这个WBTableSectionHeaderFooter是使用自动布局的方式确定高度还是使用Frame方式

(Tag3) 那么什么是数据驱动的View

iOS列表中数据驱动的View包括 UITableViewCell UICollectionViewCell UITableViewFooter&Header UICollectionViewSupplementary
这些视图的很多行为相同,比如update,reset,reload,cancel,框架会在合适的时机回调这些方法,业务方只需要在这些方法做相应的事情就可以(大家的代码又一样了)所以就有了这个协议 WBListReusableViewProtocol 所以 WBTableCellProtocal WBTableHeaderFooterViewProtocal 都是 WBListReusableViewProtocol 的子协议, WBTableCellProtocal中有row属性,WBTableHeaderFooterViewProtocal中有headerfooter属性,这样所有的cell和footerheader都有了row模型,同时也拥有了WBListReusableViewProtocol中的框架回调方法,还担心大家的代码不一致吗?
同时这些View还存在向外部抛出事件的需求,那么所有遵循 WBListActionToControllerProtocol 的对象都可以接受抛出事件的回调,大部分来讲这个对象是控制器
这样所有的事件都有迹可循,每个人写的代码都八九不离十。 WBListActionToControllerProtocol 同时也继承了 UITableViewDelegate协议,结果所有事件都可以通过一个代理搞定,这个协议如下:

/**
 比如Cell中有一个button,需要到Controller中发送网络请求,那么代码如下:
 'button.action = ^(){
     [self.actionDelegate actionFromView:self withEventTag:@"youreventtage" withParameterObject:self.row];
 };'
 
 actionDelegate是WBListReusableViewProtocol中的一个可选属性,业务方可以合成这个属性,将事件分发出去
*/

@protocol WBListActionToControllerProtocol <NSObject,
                                UITableViewDelegate,
                                UICollectionViewDelegateFlowLayout,
                                UICollectionViewDelegate>
@optional

- (void)actionFromReusableView:(UIView *)view
                      eventTag:(NSString *)tag
                     parameter:(id)param;

@end

WBTableViewAdapter

  • 实现了 UITableView 的所有数据源和大部分代理方法,而且通过拦截者方式,所以这些代理对业务方完全透明
  • 自动注册 UITableViewCell UICollectionViewCell UITableViewFooter&Header UICollectionViewSupplementary
  • 通过updateSection,addSection,deleteSection等操作给View装配数据,上代码:
self.adapter = [[WBTableViewAdapter alloc] init];
self.tableView bindAdapter:self.adapter];
[self.adapter addSection:^(WBTableSection * _Nonnull section) {
        
        for (NSInteger index = 0; index < 5; ++index) {
            WBTableRow *row = [[WBTableRow alloc] init];
            row.associatedCellClass = [WBSimpleListCell class];
            row.data = @{@"title":@(index)
                            };
            maker.addRow(row);
        }
      
        [section addRow:row];
        section.key = @"FixedHeightSection"
    }];

Cell中的代码

@implementation WBSimpleListCell

@synthesize row = _row;

- (void)makeLayout{
    
    [self.label mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.contentView);
    }];
}

//框架在合适的时机调用这个方法,业务方根据self.row.data中的数据更新显示
- (void)update{
    self.label.text = [NSString stringWithFormat:@"SimpleList self manage height Cell Index : %@",[[(NSDictionary *)self.row.data objectForKey:@"title"] stringValue]];
}

@end

(Tag2) 我们给Cell提供什么样的数据

第一种方式,不论什么情况下,直接将网络请求或者本地加载的数据,用原始类型(NSDictionary,NSArray...)提供给row的data属性,在cell的update方法中直接访问原始类型
这种方式很好,不会造成类爆炸,可以在调试的时候直接打印,很直观,但是并不完美,如果一个cell有自己的状态,比如是否选中,比如是否在播放中,是否浏览过,这些状态如果我们也追加到原始类型中,就会出现表意不明,如果没有定义好足够明确的key,那么后期维护是很恐怖的。即便强制要求代码规范,但是项目是在生长的,很难讲什么时候走偏。
第二种方式是通过映射(MJExtension,YYModel)等的方式将网络返回的原始数据转换(不讨论转换代码的位置,只讨论交付的数据)成Model交付给Cell,这样就解决了上面的问题,但是坏处也是显而易见的,会造成类爆炸,即便是简单的表单提交页面,也会写出很多不必要的Model。

针对上述问题,框架提供了一个协议 WBListDataReformerProtocol 协议长这个样子:

@protocol WBListDataReformerProtocol <NSObject>

- (void)reformRawData:(id)data forRow:(WBTableRow *)row;

@optional
@property (nonatomic, strong) id rawData;

@end

对于没有状态的Cell,使用原始数据类型表征就很清楚了,可以直接将原始类型扔给Cell用。
对于带有自有状态,或者需要原始数据转化后才能显示的需求,创建一个遵循这个协议的对象,自定义对象属性,提供给Cell使用,代码如下:

[self.adapter addSection:^(WBTableSectionMaker * _Nonnull maker) {
        
        for (NSInteger index = 0; index < 5; ++index) {
            WBTableRow *row = [[WBTableRow alloc] init];
            row.associatedCellClass = [WBReformerListCell class];
            WBReformerListCellReformer *reformer = [WBReformerListCellReformer new];
            [reformer reformRawData:@{@"title":@(index),
                                      @"date":[NSDate new]
                                      } forRow:row];
            row.data = reformer;
            maker.addRow(row);
        }
    }];

Reformer代码

@interface WBReformerListCellReformer : NSObject<WBListDataReformerProtocol>

@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, copy, readonly) NSString *date;

@end

@interface WBReformerListCellReformer ()

@property (nonatomic, copy, readwrite) NSString *title;
@property (nonatomic, copy, readwrite) NSString *date;

@end

@implementation WBReformerListCellReformer

@synthesize rawData = _rawData;

- (void)reformRawData:(id)data forRow:(WBTableRow *)row{
    
    if (![data isKindOfClass:[NSDictionary class]]) {
        return;
    }
    self.rawData = data;
    self.title = [[data objectForKey:@"title"] stringValue];
    NSDate *date = [data objectForKey:@"date"];
    self.date = [date description];
}

@end

Cell中代码:

- (void)update{
    WBReformerListCellReformer *reformer = (WBReformerListCellReformer *)self.row.data;
    self.title.text = reformer.title;
    self.date.text = reformer.date;
}

内建的自动Differ刷新功能

我们都有这样的经验,当我们需要删除一些rows或者sections的时候,要频繁的调用deleteRow insertRow deleteSection等操作,同时还要同步数据和UI显示, 一不小心就容易崩溃。第二当我们加载更多的时候,为了避免如上操作,经常会reload整个view,造成不必要的计算浪费(重新reload势必要重新加载cell中的数据,入如果有复杂的cell,势必造成IO)。现在框架内部提供了数据Differ,功能,通俗讲就是比较两个数组,将数组变化的部分转化成 insert delete move 等操作,然后在合适的时候批量的提交给View刷新,所以业务方可以尽情的delete row insert,框架会在合适的时机提交给view更新。具体提供了如下三个方法

- (void)beginAutoDiffer;
- (void)commitAutoDifferWithAnimation:(BOOL)animation;
- (void)reloadDifferWithAnimation:(BOOL)animation;

所有在begin和commit中间进行的操作都会在commit的一刻提交给view显示,可以指定是否要动画。reload方法会随时将和上次reload或者commit之后的更改提交给View更新,代码如下:

[self.tableViewAdapter beginAutoDiffer];
    [self.tableViewAdapter deleteAllSections];
    [self.tableViewAdapter commitAutoDifferWithAnimation:NO];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_async(dispatch_get_main_queue(), ^{
            
            [self.tableViewAdapter beginAutoDiffer];
            [self.tableViewAdapter addSection:^(WBTableSection * _Nonnull section) {
                
                [section setIdentifier:@"fangyuxi"];
                for (NSInteger index = 0; index < 15; ++index) {
                    WBTableRow *row = [[WBTableRow alloc] init];
                    row.calculateHeight = ^CGFloat(WBTableRow *row){
                        return 60.0f;
                    };
                    row.associatedCellClass = [WBReformerListCell class];
                    WBReformerListCellReformer *reformer = [WBReformerListCellReformer new];
                    [reformer reformRawData:@{@"title":@(index),
                                              @"date":[NSDate new]
                                              } forRow:row];
                    row.data = reformer;
                    [section addRow:row];
                }
            }];
            [self.tableViewAdapter commitAutoDifferWithAnimation:NO];
            
            self.canLoadMore = YES;
            [self notifyDidFinishLoad];
        });

(Tag3) 下面解决数据从哪里来的问题

从上面我们可以看出来,Adapter其实只是一个数据的装配器(不同于Android),UITableView的Adapter装配适合UITableView的数据,UICollectionView的Adapter装配适合UICollectionView的数据,它并不关心数据从哪里来。那么如果是非常简单的数据源,比如一个关于页面中只有两列,版本信息和版权信息,而且也没有下拉刷新等功能,那么直接用Controller提供数据未尝不可,所有逻辑都集中在一个类中,比如下面的代码:

@interface WBListKitDemosViewController ()<WBListActionToControllerProtocol>
@property (nonatomic, strong) WBTableViewAdapter *adapter;
@property (nonatomic, strong) UITableView *tableView;
@end

@implementation WBListKitDemosViewController

- (void)viewDidLoad {
    
    [super viewDidLoad];
    self.title = @"WBListKit Demos";
    self.view.backgroundColor = [UIColor redColor];
    
    self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen     mainScreen].bounds.size.height) style:UITableViewStylePlain];
    [self.view addSubview:self.tableView];
    
    self.adapter = [[WBTableViewAdapter alloc] init];
    [self.tableView bindAdapter:self.adapter];
    self.tableView.actionDelegate = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [self loadData];
    });
}

- (void)loadData{
    
    // hide warnings
    __weak typeof(self) weakSelf = self;
    [self.adapter addSection:^(WBTableSection * _Nonnull section) {
        
        NSMutableArray *rows = [NSMutableArray new];
        [[weakSelf data] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
           
            WBTableRow *row = [[WBTableRow alloc] init];
            row.calculateHeight = ^CGFloat(WBTableRow *row){
                return 60.0f;
            };
            row.associatedCellClass = [WBDemosCell class];
            row.data = obj;
            [rows addObject:row];
            
        }];
 
        [section addRows:rows]
        section.key = @"DemoIdentifier"
    }];
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        dispatch_async(dispatch_get_main_queue(), ^{

            [self.tableView reloadData];
        });
    });
}

- (NSArray *)data{
    return @[@{@"title":@"Simple List",@"class":[WBSimpleListViewController class]},
             @{@"title":@"Expanding Cell List",@"class":[WBExpandingCellViewController class]},
             @{@"title":@"Reformer List",@"class":[WBReformerListViewController class]},
             @{@"title":@"FooterHeader List",@"class":[WBListHeaderFooterViewController class]},
             @{@"title":@"MVC Demos",@"class":[WBMVCViewController class]},
             @{@"title":@"Multi DataSource",@"class":[WBMultiSourceController class]},
             @{@"title":@"CollectionView",@"class":[WBCollectionViewController class]},
             @{@"title":@"Nested",@"class":[WBNestedViewController class]},
             @{@"title":@"Custom Layout",@"class":[WBCustomLayoutViewController class]},
             @{@"title":@"WaterFall Layout",@"class":[WBWaterFallViewController class]},
             @{@"title":@"Empty Kit Swift ",@"class":[WBSwiftEmptyViewController class]},
             @{@"title":@"Empty Kit OC ",@"class":[WBOCEmptyViewController class]}
             ];
}
@end

以上代码,提供数据源的方法 - (NSArray *)data 加载数据的方法 - (void)loadData ,这样些并没有什么问题,但是如果提供数据源方法业务逻辑复查杂(查询数据库,加载网络,缓存),而且还要支持上拉刷新等操作,都写在控制器里面,坏处是显而易见的,那么这个时候我们就急需要一个Model层和一个Dao层,所以框架提供了列表类的数据源 WBListDataSource 此类定义了外部(通常是控制器)操作数据源的接口loadSource loadMoreSource cancelLoad和属性 canLoadMore,对外部提供代理方法 WBListDataSourceDelegate 汇报自身状态:

@protocol WBListDataSourceDelegate <NSObject>

@optional

- (void)sourceDidStartLoad:(WBListDataSource *)tableSource;
- (void)sourceDidFinishLoad:(WBListDataSource *)tableSource ;
- (void)sourceDidStartLoadMore:(WBListDataSource *)tableSource;
- (void)sourceDidFinishLoadMore:(WBListDataSource *)tableSource;

- (void)source:(WBListDataSource *)tableSource loadError:(NSError *)error;
- (void)source:(WBListDataSource *)tableSource loadMoreError:(NSError *)error;
- (void)source:(WBListDataSource *)source didReceviedExtraData:(id)data;

- (void)sourceDidClearAllData:(WBListDataSource *)tableSource;

@end

子类可以在合适的时机调用如下这些方法驱动delegate:

@interface WBListDataSource (NotifyController)

- (void)notifyWillLoad;
- (void)notifyWillLoadMore;
- (void)notifyDidFinishLoad;
- (void)notifyDidFinishLoadMore;
- (void)notifyDidReceviedExtraData:(nonnull id)data;
- (void)notifyLoadError:(nonnull NSError *)error;
- (void)notifyLoadMoreError:(nonnull NSError *)error;
- (void)notifySourceDidClear;

@end

并提供了两个子类 WBTableViewDataSource WBCollectionViewDataSource 、两个子类中分别带有 UITableViewAdapter UICollectionViewAdapter 用于拼装数据。用这种方法之后代码见 MVC 文件夹。并且当页面中存在多种数据源的时候,只需要切换就可以了,代码见 MultiDataSource 文件夹,代码就不大段贴了,只贴一部分控制器的代码,可以看出来,很精简。

@interface WBMVCViewController ()<WBListActionToControllerProtocol>

@end

@implementation WBMVCViewController

- (void)viewDidLoad {
    
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    
    [self createView];
    self.list.tableDataSource = [[WBMVCTableListDataSource alloc] initWithDelegate:self];
    [self.list.tableView bindViewDataSource:self.list.tableDataSource];
    
    [self.list refreshImmediately];
}

- (void)createView{
    WBMVCRefreshHeader *header = [[WBMVCRefreshHeader alloc] init];
    self.list.refreshHeaderControl = header;
    
    self.list.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width,           [UIScreen mainScreen].bounds.size.height) style:UITableViewStylePlain];
    [self.view addSubview:self.list.tableView];
    
    WBMVCRefreshFooter *footer = [[WBMVCRefreshFooter alloc] init];
    self.list.loadMoreFooterControl = footer;
}

- (void)sourceDidStartLoad:(WBListDataSource *)tableSource{
    
}

- (void)actionFromReusableView:(UIView *)view eventTag:(NSString *)tag parameter:(id)param{
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
    
}

@end

看不明白self.list是什么?让我们更进一步

到目前为止,之前提出的问题大部分都解决了,只是还剩一个,下拉刷新和上拉加载更多怎么支持?这些动作其实是View层发出的action,我们应该交由Controller层响应事件,然后通知DataSource刷新数据,然后Controller再响应DataSource的回调刷新View,这个流程是所有业务方都有的流程, 那么我们是不是可以提供一个列表控制器的基类来完成一些View和DataSource的协调工作呢,这样子类就天然拥有了这些协调工作,可以,但是不够完美,因为看到了继承,而且还是控制器的继承,一旦我们要求业务方继承一个控制器,那么就很难将框架应用到现有的项目中,而且继承是强耦合的范式,也不方便扩展。那用协议怎么样?协议是最好的解决办法,但是我们不仅需要统一的接口,也需要一些实现好的接口,OC语言也天然没有支持协议默认实现的功能(Swift中的协议扩展适合解决这个问题,OC可以通过ProtocolKit解决).那么我们可以给UIViewController提供一个proxy对象,通过组合的方式给UIViewController提供列表功能,这个属性叫list(只要在现有项目中控制器没有这个list属性的命名冲突就可以0成本接入了),list是一个 WBListController类型的实例,继承自(NSObject),提供了全套的列表功能支持,代码如下:

@interface WBListController : NSObject<WBListDataSourceDelegate>

/**
 创建列表控制器

 @param viewController 'UIViewController'
 @return listController
 */
- (nullable instancetype)initWithController:(nonnull UIViewController *)viewController;

/**
 refresh
 */
- (void)dragToRefresh; //会引发refreshHeaderControl的刷新动画(调用refreshHeaderControl中的begin方法)
- (void)refreshImmediately; //直接刷新数据源,不会引发refreshHeaderControl变化

/**
 加载更多的两种方式,通常,这两个方法需要手工调用的情况很少,加载更多的控件会在合适的时机自动调用它们
 我能想到的场景就是在做预加载的情况下可以调用
 */
- (void)dragToLoadMore;
- (void)loadMoreImmediately;

/**
 提供一个WBTableViewDataSource和UITableView
 注意不能同时存在UITableView和UICollectionView,如果同时存在会产生异常
 */
@property (nonatomic, strong, nullable) WBTableViewDataSource *tableDataSource;
@property (nonatomic, strong, nullable) UITableView *tableView;

/**
 提供一个WBCollectionViewDataSource和UICollectionView
 注意不能同时存在UITableView和UICollectionView,如果同时存在会产生异常
 */
@property (nonatomic, strong, nullable) WBCollectionViewDataSource *collectionDataSource;
@property (nonatomic, strong, nullable) UICollectionView *collectionView;


/**
 集成下拉刷新和上拉加载更多的接口,框架内部会在合适的时机调用接口中定义的方法
 */
@property (nonatomic, strong, nullable) id<WBListRefreshHeaderViewProtocol> refreshHeaderControl;
@property (nonatomic, strong, nullable) id<WBListRefreshFooterViewProtocol> loadMoreFooterControl;

@end

WBListController控制了MVC的数据流转

  • 通过接口规范了同刷新控件的交互方式,可以插件化的提供刷新控件
  • 响应刷新控件的消息,进而控制数据源
  • 响应数据源回调,刷新View

如果你的业务不是上述加粗字体的流程,那么可以定义自己的 " WBListController "

刷新控件

利用如下四个协议:WBListRefreshControlCallbackProtocol WBListRefreshControlProtocol WBListRefreshFooterViewProtocol WBListRefreshHeaderViewProtocol ,一个实现好的刷新控件:

#import <MJRefresh/MJRefresh.h>
#import "WBListRefreshHeaderViewProtocol.h"

@interface WBMVCRefreshHeader : MJRefreshStateHeader<WBListRefreshHeaderViewProtocol>

@end

@interface WBMVCRefreshHeader ()
@property (nonatomic, weak) UIScrollView *attchedView;
@end

@implementation WBMVCRefreshHeader

- (void)begin{
    [self beginRefreshing];
}

- (void)end{
    [self endRefreshing];
}

- (void)attachToView:(UIScrollView *)scrollView
      callbackTarget:(id<WBListRefreshControlCallbackProtocol>)target{
    
    self.attchedView = scrollView;
    scrollView.mj_header = self;
    self.refreshingTarget = target;
    self.refreshingBlock = ^{
        [target refreshControlBeginRefreshing];
    };
}

- (void)enable{
    self.attchedView.mj_header = self;
}

- (void)disable{
    self.attchedView.mj_header = nil;
}

@end

空页面加载,错误页面,空内容提示

老生常谈的问题,当View没有内容的时候,需要给一个全屏的提示,可能是正在加载的提示,可能是加载错误并没有缓存等。框架提供了 WBListEmptyViewKit 但是并没有耦合到框架内部。使用方法见Demo,由于 WBListEmptyViewKit 是使用Swift编写,而且用了协议扩展和泛型,目前并不能和业务方的OC代码混编,如果想要OC版本,可看这里,使用方法和 WBListEmptyViewKit 一样。 其实WBListEmptyViewKit就是抄的这个。类似这样:

override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(tableView);
        tableView.frame = view.bounds
        tableView.empty.delegate = self
        tableView.empty.dataSource = self
        tableView.actionDelegate = self;
        
        tableView.bindTableView(adapter);
        
        let leftItem: UIBarButtonItem = UIBarButtonItem(title: "增加", style: UIBarButtonItemStyle.plain, target: self, action: #selector(add))
        let rightItem: UIBarButtonItem = UIBarButtonItem(title: "清空", style: UIBarButtonItemStyle.plain, target: self, action: #selector(clear))
            
        self.navigationItem.rightBarButtonItems = [leftItem, rightItem]
            
        self.loadData();
    }

extension WBSwiftEmptyViewController : WBListEmptyKitDataSource{
    
    /// 可以在这些方法中通过ViewSource的delegate方法中拿到error code 根据error code 
    /// 返回特定的view
    
    // you can try
//    func ignoredSectionsNumber(in view: UIView) -> [Int]? {
//        return [0]
//    }
    
    // you can try
//    func emptyLabel(for emptyView: UIView, in view: UIView) -> UILabel? {
//        let label = UILabel()
//        label.text = "空页面"
//        label.textAlignment = NSTextAlignment.center
//        label.backgroundColor = UIColor.red
//        return label
//    }
    
    func emptyButton(for emptyView: UIView, in view: UIView) -> UIButton? {
        let button = UIButton()
        button.setTitle("空页面按钮演示,点击事件", for: UIControlState.normal)
        button.setTitleColor(UIColor.red, for: .normal)
        button.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: 100, height: 100))
        button.layer.borderWidth = 1
        button.layer.borderColor = UIColor.red.cgColor
        
        return button
    }
}

extension WBSwiftEmptyViewController : WBListEmptyKitDelegate{
    
    func emptyView(_ emptyView: UIView, button: UIButton, tappedInView: UIView) {
        print( #function, #line, type(of: self))
    }
    
    func emptyView(_ emptyView: UIView, tappedInView: UIView) {
        print( #function, #line, type(of: self))
    }
}

UICollectionView

实现原理和用法和UITableView一样,不同点如下:

  • 高度缓存,UICollectionView的Item的Size是由布局对象决定,而且UICollectionView内部已经做好了高度缓存。
  • 提供了 WBCollectionSupplementaryItem 支持
  • 结合自定义布局 请看 CustomLayout 提供了两个例子

在UITableViewCell中嵌入UICollectionView

@implementation WBNestedTableViewCell

@synthesize row = _row;

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    
    self.contentView.backgroundColor = [UIColor redColor];
    
    UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
    layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    
    self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
    [self.contentView addSubview:self.collectionView];
    self.collectionView.backgroundColor = [UIColor yellowColor];
    self.collectionView.actionDelegate = self;
    
    [self makeLayout];
    
    self.adapter = [[WBCollectionViewAdapter alloc] init];
    [self.collectionView bindAdapter:self.adapter];
    
    return self;
}

- (void)makeLayout{
    [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.contentView);
    }];
}

- (void)update{
    [self.adapter deleteAllElements];
    [self.collectionView reloadData];
    
    // data from ... anywhere
    
    [self.adapter addSection:^(WBCollectionSection * _Nonnull section) {
        maker.setIdentifier(@"WBNested");
        for (NSInteger index = 0; index < 100; ++index) {
            WBCollectionItem *item = [[WBCollectionItem alloc] init];
            item.associatedCellClass = [WBNestedCollectionViewCell class];
            item.data = @{@"title":@(index)
                          };
           [section addItem:item];
        }
    }];
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath{
    return CGSizeMake(40, 40);
}

@end

MVVM

对于表单提交类的列表来说,MVVM可能很合适,框架内部虽然没有支持,也是考虑到每个项目的MVVM选型不一样,但是Reformer机制(Tag2)已经提供了 足够多的灵活性,Reformer可以结合ReactCocoa或者KVOController来实现MVVM。

问题

Reformer的设计是否存在摇摆不定?如果框架没有一个范式来约束什么时候用原始类型什么时候用Reformer,那么业务方怎么保证用的准确?

About

A data-driven UICollectionView&UITableView framework for building fast and flexible lists by declarative syntax.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published