iOS中通用的TableView和CollectionView DataSource和Cell

剛進公司不到一個月,接到一個需求,把一個項目的界面改一下??戳隧椖坷锏囊晥D,耦合性強,沒有復用,改起UI來很費勁。新的界面是個列表視圖,就尋思怎么寫出一個后面接手的人改起UI不那么費勁的tableview,順便把項目里老的collectionView也進行了類比,偷偷進行了重構。畢竟UI總是改來改去的,而常用的tableview和collecitonview更是應該讓其擴展性更好,復用性更強。

先從tableview講起,tableview都很熟悉,一般就是由datasource,delegate,cell,cellitem這幾部分組成。

打造一個通用性更強的DataSource

新建一個NSObject的子類,命名為CDZTableDataSource,并遵循<UITableViewDataSource>協(xié)議。對于datasource來說,必須實現(xiàn)的方法是-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

先看看第一個方法,返回每個section的row的數(shù)目,這個數(shù)字怎么來呢,就是section里的數(shù)據(jù)條數(shù)。如果不實現(xiàn)optional方法

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView的話,默認只有一個section。所以我們首先要確定的就是section的數(shù)目,所以對應的,我們應該有section的model。新建一個NSObject的子類,命名為CDZSectionObject。那么這個section應該有些什么屬性呢?對于一個section對象來說,應該持有一個row對應的model的數(shù)組,這樣就可以確定每個section的row的數(shù)量了??赡苓€有一些sectionheader是數(shù)據(jù)等等。

@property (nonatomic, copy) NSString *headerTitle;
@property (nonatomic, copy) NSString *headerReuseIdentifer;
@property (nonatomic, strong) NSMutableArray *itemsArray;

回到datasouce,那么datasouce就可以加上這些屬性。

@property (nonatomic, strong) NSMutableArray<CDZSectionObject *> *sectionsArray;
@property (nonatomic, strong) CDZSectionObject *firstSection;   

那么現(xiàn)在就可以實現(xiàn)第一個方法了,為了方便我們還可以默認有一個firstsection,方便只有一個section的情況使用。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    if (self.sectionsArray.count > section) {
        return self.sectionsArray[section].itemsArray.count;
    }
    return 0;
}


- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
    return self.sectionsArray ? self.sectionsArray.count : 0;
}

- (CDZSectionObject *)firstSection{
    if (!_firstSection) {
        _firstSection = [[CDZSectionObject alloc]init];
        [self.sectionsArray addObject:_firstSection];
    }
    return _firstSection;
}

輪到第二個方法了,配置cell的時候,我們需要知道cell對應的item而去配置cell,那么更抽象一點的話,也就是說datasource是建立了cellitem和cell之間的聯(lián)系,也就是Model-View直接的聯(lián)系。View本身做的工作也就是根據(jù)Model配置View的樣子。所以這時候我們我們可以定義一個接口。

@protocol CDZCustomView
@required
- (void)setItem:(id)item;
@end

然后讓我們的Cell遵守這個協(xié)議,也就是所有通用的Cell,需要在方法內(nèi)實現(xiàn)item的解析。而其實對于cell也是一種view,這個接口可以讓一些復用的view都遵守,比如headerview等等。對于cell我們還可以拓展這個協(xié)議。

@protocol CDZTableCell <CDZCustomView>
@required
+ (CGFloat)tableView:(UITableView *)tableView rowHeightForItem:(id)item;
@end

這樣,cell的高度也可以由cell內(nèi)部自己決定,適合各種各樣的cell,當然也可以讓autolayout決定。但是有一些老項目里的cell里面沒有autolayout,所以返回高度通用性可能更強些。

回到datasource,我們解決了cell的model問題,下一個是,cell的class怎么決定呢?其實對于datasouce來說,cell的類型,往往是由對應的model決定的,也就是model的類型,決定可以使用的cell的種類。那么我們在datasouce里,只要給出一個接口,讓使用者建立這種聯(lián)系,item class - cell class的聯(lián)系,并在內(nèi)部用一個類似哈希表的東西儲存起來,我們便可以根據(jù)item找到cell的class。簡單的話用一個NSMutableDictionary就可以了。

- (void)setCellClass:(Class)cellClass forItemClass:(Class)itemClass{
    if (!itemClass || !cellClass) {
        return;
    }
    [self.cellDic setObject:cellClass forKey:NSStringFromClass(itemClass)];
}

而這樣以后要更換view的時候,只要新建一個cell并重新set一下對應的item的就可以了,換model也類似。

那么現(xiàn)在就可以根據(jù)index找到對應的section和對應的row所對應的item所對應的cellclass了。

- (id)tableView:(UITableView *)tableView itemForRowAtIndexPath:(NSIndexPath *)indexPath{
    if (self.sectionsArray.count > indexPath.section) {
        if (self.sectionsArray[indexPath.section].itemsArray.count > indexPath.row) {
            return self.sectionsArray[indexPath.section].itemsArray[indexPath.row];
        }
    }
    return nil;
}

- (Class)tableView:(UITableView *)tableView cellClassForRowAtIndexPath:(NSIndexPath *)indexPath{
    id item = [self tableView:tableView itemForRowAtIndexPath:indexPath];
    Class cellClass = [self.cellDic objectForKey:NSStringFromClass([item class])];
    if (!cellClass) {
        cellClass = [UITableViewCell class];//沒有對應關系的,取默認的UITalbeViewCell
    }
    return cellClass;
}

那么cellforrow方法就出來了

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    Class cellClass = [self tableView:tableView cellClassForRowAtIndexPath:indexPath];
    UITableViewCell<CDZTableCell> *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellClass)];
    if (!cell) {
        cell = [[cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellClass)];
    }
    if ([cell respondsToSelector:@selector(setItem:)]) {//檢查是否有實現(xiàn)setItem的方法
        [cell setItem:[self tableView:tableView itemForRowAtIndexPath:indexPath]];
    }
    return cell;
}

到此DataSource就完成了。

如何使用呢?

讓自定義的cell遵守<CDZTableCell>協(xié)議,內(nèi)部實現(xiàn)setItem方法去配置view。一些老的cell也可以簡單的遵守協(xié)議并把以前配置視圖的方法寫到setItem方法里就可以了。協(xié)議比繼承耦合性更低些,即加即用。

讓tableview的datasouce指定為一個CDZTabeDataSource,并設置item對應的cell類型就可以。更換cell和item只要重新建立關系即可,達到了datasource的復用和拓展性。對于各種tableview,就不用重復寫datasource的代碼了。例子如Demo

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    Class cellClass = [self.tableDataSource tableView:tableView cellClassForRowAtIndexPath:indexPath];
    return (cellClass == [UITableViewCell class]) ? 44.f : [cellClass tableView:tableView rowHeightForItem:[self.tableDataSource tableView:tableView itemForRowAtIndexPath:indexPath]];
}


- (UITableView *)tableView{
    if (!_tableView) {
        _tableView = [[UITableView alloc]initWithFrame:self.view.frame style:UITableViewStylePlain];
        _tableView.delegate = self;
        _tableView.dataSource = self.tableDataSource;
    }
    return _tableView;
}

- (CDZTableDataSource *)tableDataSource{
    if (!_tableDataSource) {
        _tableDataSource = [[CDZTableDataSource alloc]init];
        [_tableDataSource setCellClass:[CDZTableViewCell class] forItemClass:[CDZCellItem class]];
        _tableDataSource.firstSection.itemsArray = [[self mockItems] copy];
    }
    return _tableDataSource;
}

CollecitonViewDatasource其實也類似

區(qū)別在于collecitonview的cell要注冊,所以cellforrow方法有所不同

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    Class cellClass = [self collectionView:collectionView cellClassForRowAtIndexPath:indexPath];
    UICollectionViewCell<CDZCollectionCell> *cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass(cellClass) forIndexPath:indexPath];
    if ([cell respondsToSelector:@selector(setItem:)]) {
        [cell setItem:[self collectionView:collectionView itemForRowAtIndexPath:indexPath]];
    }
    return cell;
}

其它基本類似,且collectionview和tableview還可以共用一個sectionobject。

最后

所有源碼和Demo
工作里,我們經(jīng)常會因為趕需求而做一些復制粘貼。但也別忘了多思考如何復用,如何類比,雖然花掉現(xiàn)在一些時間,卻會在日后的重構,修改,別人的修改上帶來很多便利。

如果您覺得有幫助,不妨給個star鼓勵一下,歡迎關注&交流
有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz

謝謝觀看

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容