推广

UITableView 建模 — 提高代码复用

iseeyu2年前 (2024-02-21)推广126

tableview 是开发中项目中常用的视图控件,并且是重复的使用,布局类似,只是数据源及Cell更改,所以会出现很多重复的内容,并且即使新建一个基础的列表也要重复这些固定逻辑的代码,这对于开发效率很不友好。
本文的重点是抽取重复的逻辑代码简化列表页面的搭建,达到数据驱动列表

思路草稿

说明:
首先tableview有两个代理delegate 和 datasource(基于单一职责设计规则)
delegate :负责交互事件;
datasource :负责cell创建及数据填充,这也是本文探讨的重点。
(1)基本原则
苹果将tableView的数据通过一个二维数组构建(组,行),这是一个很重要的设计点,要沿着这套规则继续发展,设计模式的继承,才是避免坏代码产生的基础。
(2)组
“组”是这套逻辑的根基先有组再有行,并且列表动态修改的内容都是以为基础,的结构相对固定,因此本文将抽离成一个数据模型而不是接口

#import <Foundation/Foundation.h>
#import "RWCellViewModelProtocol.h"

@interface RWSectionModel : NSObject
/// item数组:元素必须是遵守RWCellViewModel协议
@property (nonatomic, strong) NSMutableArray <id<RWCellViewModel>>*itemsArray;

/// section头部高度
@property (nonatomic, assign) CGFloat  sectionHeaderHeight;
/// section尾部高度
@property (nonatomic, assign) CGFloat  sectionFooterHeight;
/// sectionHeaderView: 必须是UITableViewHeaderFooterView或其子类,并且遵循RWHeaderFooterDataSource协议
@property (nonatomic, strong) Class headerReuseClass;
/// sectionFooterView: 必须是UITableViewHeaderFooterView或其子类,并且遵循RWHeaderFooterDataSource协议
@property (nonatomic, strong) Class footerReuseClass;

/// headerData
@property (nonatomic, strong) id headerData;
/// footerData
@property (nonatomic, strong) id footerData;
@end

(2)行
最核心的有三大CellCell高度Cell数据
这次的设计参考MVVM设计模式,对于行的要素提取成一个ViewModel,并且ViewModel要做成接口的方式,因为行除了这三个基本的元素外,可能要需要Cell填充的数据,比如titleString,subTitleString,headerImage等等,这样便于扩展。

#ifndef RWCellViewModel_h
#define RWCellViewModel_h

@import UIKit;

@protocol RWCellViewModel <NSObject>
/// Cell 的类型
@property (nonatomic, strong) Class cellClass;
/// Cell的高度:  0 则是UITableViewAutomaticDimension
@property (nonatomic, assign) CGFloat  cellHeight;
@end

#endif /* RWCellViewModel_h */

(3)tableView
此处不用使用tableViewController的方式,而使用view的方式,这样嵌入更方便。并且对外提供基本的接口,用于列表数据的获取,及点击事件处理。

备注:
关于数据,这里提供了多组和单组的两个接口,为了减少使用的过程中外部新建RWSectionModel这一步,但是其内部还是基于RWSectionModel这一个模型。

#import <UIKit/UIKit.h>
#import "RWCellViewModelProtocol.h"
#import "RWSectionModel.h"

@protocol RWTableViewDelegate;

@interface RWTableView : UITableView
/// rwdelegate
@property (nonatomic, weak) id<RWTableViewDelegate> rwdelegate;

/// 构建方法
/// @param delegate 是指rwdelegate
- (instancetype)initWithDelegate:(id<RWTableViewDelegate>)delegate;

@end


@protocol RWTableViewDelegate <NSObject>
@optional
/// 多组构建数据
- (NSArray <RWSectionModel*>*)tableViewWithMutilSectionDataArray;

/// 单组构建数据
- (NSArray <id<RWCellViewModel>>*)tableViewWithSigleSectionDataArray;


/// cell点击事件
/// @param data cell数据模型
/// @param indexPath indexPath
- (void)tableViewDidSelectedCellWithDataModel:(id)data indexPath:(NSIndexPath *)indexPath;
RWTableview.m

#pragma mark - dataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    /// 数据源始终保持“二维数组的状态”,即SectionModel中包裹items的方式
    if ([self.rwdelegate respondsToSelector:@selector(tableViewWithMutilSectionDataArray)]) {
        self.dataArray = [self.rwdelegate tableViewWithMutilSectionDataArray];
        return self.dataArray.count;
    }
    else if ([self.rwdelegate respondsToSelector:@selector(tableViewWithSigleSectionDataArray)]) {
        RWSectionModel *sectionModel = [[RWSectionModel alloc]init];
        sectionModel.itemsArray = [self.rwdelegate tableViewWithSigleSectionDataArray].mutableCopy;
        self.dataArray = @[sectionModel];
        return 1;
    }
    return 0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:section];
    return sectionModel.itemsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    /// 此处只做Cell的复用或创建
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
    id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
    if (cell == nil) {
        cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
    }
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}

(4)Cell上子控件的交互事件处理
腾讯QQ部门的大神峰之巅提供了一个很好的解决办法,基于苹果现有的响应链(真的很牛逼),将点击事件传递给下个响应者,而不需要为事件的传递搭建更多的依赖关系。这是一篇鸡汤文章,有很多营养,比如tableview模块化,这也是我接下来要学习的。

#import <UIKit/UIKit.h>
#import "RWEvent.h"

@interface UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event;

@end
#import "UIResponder+RWEvent.h"

@implementation UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event {
    [self.nextResponder respondEvent:event];
}

@end

2020年11月18日 更新

鉴于此tableView封装在实际项目遇到的题进行改善,主要内容如下:
(1)使用分类的方式替换协议
优点:分类能更便捷的扩展原有类,并且使用更方便,不需要再导入协议文件及遵守协议
【RWCellDataSource协议】替换成:【UITableViewCell (RWData)】
【RWHeaderFooterDataSource协议】 替换成:【UITableViewHeaderFooterView (RWData)】

(2)cell高度缓存的勘误
willDisplayCell:中要想获取准确的Cell高度,那么必须在heightForRowAtIndexPath:方法中给Cell赋值,因为系统计算Cell的高度是在这个方法中进行的

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
    id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
    /// Cell创建
    if (cell == nil) {
        cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
    }
    /// Cell赋值
    [cell rw_setData:cellViewModel];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
    id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
    return cellViewModel.cellHeight ? : UITableViewAutomaticDimension;
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
    id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
    /// 高度缓存
    /// 此处高度做一个缓存是为了高度自适应的Cell,避免重复计算的工作量,对于性能优化有些帮助
    /// 如果想要在willDisplayCell获取到准确的Cell高度,那么必须在cellForRowAtIndexPath:方法给Cell赋值
    /// 同时可以避免由于高度自适应导致Cell的定位不准确,比如置顶或者滑动到某一个Cell的位置
    /// 如果自动布局要更新高度,可以将cellViewModel设置为0
    cellViewModel.cellHeight = cell.frame.size.height;
}

完整代码

特别感谢以下作者写的文章,给我很多启发

峰之巅:iOS 高效开发解决方案

donggelaile:一站式搭建各种滑动列表(Objective-C)

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

利用MVVM设计快速开发个人中心、设置等模块

如何优雅的插入广告

iOS面向切面的TableView-AOPTableView

iOS面向切面的TableView-AOPTableView

扫描二维码推送至手机访问。

版权声明:本文由西安泽虎代运营发布,如需转载请注明出处。

转载请注明出处https://www.0291.com.cn/post/56616.html

相关文章

卖豪车的4S店有多赚钱?中升控股卖一辆车赚1.41万元?|?中报看台

卖豪车的4S店有多赚钱?中升控股卖一辆车赚1.41万元?|?中报看台

卖豪车的4S店到底有多?中升控股(00881.HK)的半年报显示,平均卖一辆车产生的净利润大约为1.41万元。中升控股经营当中,有超过四成收入来自奔驰,而算入雷克萨斯、宝马、奥迪等,则接近六成的属于豪华,其他则有丰田等部分合资品牌的销售。公布半年报后,中升控股累计下跌超过1...

什么叫“穷人思维”呢?

所谓【富人】站在商人的角度来讲他们考虑的是长期收益;而拥有【穷人思维】的人总是专注于短期回报不注重长远投资,往往会沦落为最终的失败者。下面是穷人的现金流图:下面是中产阶级的现金流图:下面是富人的现金流图:看完图,你现在发现区别了吧?如果你想致富,请记住下面这些话:...

23 FALL |波士顿大学-全球市场营销管理硕士项目解读

23 FALL |波士顿大学-全球市场营销管理硕士项目解读

波士顿大学(Boston University) 创校于1839年,是一所历史悠久的顶级私立院校,同时也是全美第三大私立大学。波士顿大学位于波士顿市中心,与哈佛、麻省理工等著名院校隔河相对,校园闹中取静,交通便利,地下铁横穿校园,又临查理士河畔,是一所拥有理想学习环境的大学...

金腾科技信息(深圳)有限公司

金腾科技信息(深圳)有限公司

金腾科技信息(深圳)有限公司2022届校园招聘公司简介金腾科技信息(深圳)有限公司(以下简称“金腾科技”)是中国国际金融股份有限公司(以下简称“中金公司”)与腾讯(深圳)有限公司(以下简称“腾讯”)合资成立的技术公司。既是中金体系下的“数字创新”,又同时协助打造腾讯生态内的...

Alexa排名是什么。

Alexa排名是什么。

整个互联网现在一共存在1亿多个网站,平均来讲每10个网民中,有一个人拥有网站。但,网站和网站是有很大区别的,不能说你有网站,我也有网站,咱们就水平相当。阿里巴巴网站和十万个为什么网站,显然水平不相当。 那么,有没有那么一个标准来衡量网站的水平呢? 美国的一群年轻人想到了一个办法:采用某个网站的用户使...

百度应该拆分吗?

百度应该拆分吗?

3月1日下午,百度发布了2021年第四季度及全年业绩。     上季度,百度总营收为330.9亿元人民币,同比增长9%,高于市场平均预期的322亿元;不按美国通用会计准则(Non-GAAP),净利润40.8亿元,同比下滑41%,但仍高于市场平均预...

现在,非常期待与您的又一次邂逅

我们努力让每一部企业宣传片和抖音短视频成为商业大片