【OC】自定义Cell
自定义Cell
文章目录
自定义Cell - Cell复用
- UITableView创建方式
- Cell复用原理
- 自定义Cell
- 两种自定义方式
在 iOS 开发中,列表界面几乎无处不在,而
UITableView是实现列表最常用的组件,系统为我们提供了默认的UITableViewCell,可以快速搭建简单的列表样式,但在实际开发中,这些默认样式往往难以满足复杂的 UI 需求,例如,一个常见的列表项可能包含头像、标题、副标题、时间、按钮等多个元素,并且还需要控制它们之间的间距和布局因此,我们通常需要通过**“自定义 Cell”**的方式,来构建更加灵活、可扩展的列表项界面
在开始之前,我们先来简单复习一下UITableView的相关知识
Cell复用
UITableView继承于UIScrollView,拥有两个相关协议UITableViewDelegate和UITableViewDataSource
UITableView是表,UITableViewCell是行,dataSource提供数据,**delegate **处理交互
UITableView创建方式
在使用 UITableView 时,创建 Cell 主要有两种方式:非注册方式和注册方式
它们的核心区别在于Cell 的创建和复用机制是否由系统统一管理
- 非注册方式
非注册方式是较早期常见的写法,需要开发者手动判断 Cell 是否存在,如果不存在则自行创建
由开发者手动创建Cell,复用时调用dequeueReusableCellWithIdentifier:
-(void)viewDidLoad{[superviewDidLoad];self.tableView=[[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStylePlain];self.tableView.delegate=self;self.tableView.dataSource=self;[self.view addSubview:self.tableView];// 不注册,什么都不写}-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{// 没注册,取不到时返回 nil,需要手动创建UITableViewCell*cell=[tableView dequeueReusableCellWithIdentifier:@"cell"];if(cell==nil){// 队列里没有可复用的,手动 alloc initcell=[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];}cell.textLabel.text=[NSString stringWithFormat:@"第 %ld 行",indexPath.row];returncell;}- 注册方式
先在初始化时注册 Cell,然后直接从复用池中获取,不需要再判断cell == nil
由系统自动创建Cell,复用时调用dequeueReusableCellWithIdentifier:forIndexPath:
// ViewController.h@interfaceViewController:UIViewController<UITableViewDelegate,UITableViewDataSource>@property(nonatomic,strong)UITableView*tableView;@end// ViewController.m-(void)viewDidLoad{[superviewDidLoad];self.tableView=[[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStylePlain];self.tableView.delegate=self;self.tableView.dataSource=self;[self.view addSubview:self.tableView];// 注册 cell[self.tableView registerClass:[UITableViewCell class]forCellReuseIdentifier:@"cell"];}-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section{return20;}-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{// 直接取,取不到系统自动创建,永远不返回 nilUITableViewCell*cell=[tableView dequeueReusableCellWithIdentifier:@"cell"forIndexPath:indexPath];cell.textLabel.text=[NSString stringWithFormat:@"第 %ld 行",indexPath.row];returncell;}Cell复用原理
当我们有很多条数据列表,而每一条都要创建自己的Cell,就会导致内存占用过高,创建视图开销大导致卡顿
为了解决这个问题,UITableView 引入了复用机制:只创建当前屏幕可见的 Cell,滚动时重复利用已经创建过的 Cell
就是说假如一个屏幕中最上面的单元格Cell要离开屏幕时就把它加入复用池,而屏幕下方即将要出现的单元格在创建自己的Cell的时候会先去复用池里找有没有可用的Cell,没有再新建
在UITableView滚动的过程中,会使用复用机制进行对单元格对象的管理,避免了频繁创建和销毁单元格,以达到提高性能和内存的利用率
当然在我们第一次加载的时候,会先调numberOfRowsInSection计算哪些行在屏幕可见范围内,然后连续快速调用cellForRowAtIndexPath:多次,每次indexPath不同,cellForRowAtIndexPath:会在需要显示某一行 Cell 时被调用,无论该 Cell 是新创建的,还是从复用池中取出的
UITableView 的 Cell 复用主要依赖一个“复用队列”(Reuse Queue)
当 Cell 滑出屏幕时,并不会被销毁,而是被放入复用队列中;当需要显示新的 Cell 时,优先从该队列中取出可复用的实例,如果没有可用的 Cell,才会创建新的实例
需要注意的是:
- 当前屏幕上正在显示的 Cell,并不属于复用队列的一部分
- UITableView 不会缓存所有 Cell,而是按需创建和复用
- 数据的管理由 dataSource 负责,而不是 UITableView 内部缓存
因此,Cell 复用的核心可以概括为:“只创建必要的 Cell,其余通过复用队列循环利用”
自定义Cell
由于系统给出的cell只能够实现文字,当默认 Cell 无法满足需求时,我们需要使用自定义 Cell来生成我们想要的单元格样式
本质就是创建一个继承于UITableViewCell的子类,复写函数
我们先来看一个简单的示范(一些知识点在注释里备注)
// CustomCell.h// 新建一个继承于UITableViewCell的类,这就是我们的自定义Cell#import<UIKit/UIKit.h>NS_ASSUME_NONNULL_BEGIN@interfaceCustomCell:UITableViewCell// 自定义Cell控件@property(nonatomic,strong)UILabel*titleLabel;@property(nonatomic,strong)UILabel*subLabel;@property(nonatomic,strong)UISwitch*swi;@endNS_ASSUME_NONNULL_END//// CustomCell.m#import"CustomCell.h"#import<Masonry/Masonry.h>@implementationCustomCell-(void)awakeFromNib{[superawakeFromNib];// Initialization code}-(void)setSelected:(BOOL)selected animated:(BOOL)animated{[supersetSelected:selected animated:animated];// Configure the view for the selected state}// 这是 cell 的指定初始化方法,系统创建 cell 时自动调用// 每次需要创建一个新的cell的时候就会调用,但是创建了之后就不会调用了,之后会进入复用池进行复用机制-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString*)reuseIdentifier{self=[superinitWithStyle:style reuseIdentifier:reuseIdentifier];if(self){self.titleLabel=[[UILabel alloc]init];self.titleLabel.text=@"标题";self.titleLabel.textColor=[UIColor blueColor];self.titleLabel.font=[UIFont systemFontOfSize:20];// contentView放自定义内容的区域,所有自定义子视图必须加在这里[self.contentView addSubview:self.titleLabel];self.subLabel=[[UILabel alloc]init];self.subLabel.text=@"副标题";self.subLabel.textColor=[UIColor grayColor];self.subLabel.font=[UIFont systemFontOfSize:16weight:UIFontWeightMedium];[self.contentView addSubview:self.subLabel];self.swi=[[UISwitch alloc]init];[self.contentView addSubview:self.swi];// 约束每一个单元格中控件的布局[self.swi mas_makeConstraints:^(MASConstraintMaker*make){make.left.equalTo(self.contentView).offset(10);make.centerY.equalTo(self.contentView);make.width.height.mas_equalTo(50);}];[self.titleLabel mas_makeConstraints:^(MASConstraintMaker*make){make.left.equalTo(self.swi.mas_right).offset(30);make.top.equalTo(self.swi).offset(5);}];[self.subLabel mas_makeConstraints:^(MASConstraintMaker*make){make.top.equalTo(self.titleLabel.mas_bottom).offset(2);make.left.equalTo(self.titleLabel);}];};returnself;}// 布局刷新时调用// 用 Masonry 时基本不需要重写 layoutSubviews,Masonry 自动处理布局刷新// 只有用 frame 手动布局时才需要写这里//- (void)layoutSubviews {// [super layoutSubviews]; // 必须调,让系统先算好 contentView 的尺寸//// // 这里能拿到真实的 frame,适合做依赖尺寸的计算// CGFloat width = self.contentView.bounds.size.width;// self.titleLabel.frame = CGRectMake(60, 10, width - 76, 20);//}@end// ViewController.h#import<UIKit/UIKit.h>#import<Masonry/Masonry.h>@interfaceViewController:UIViewController<UITableViewDelegate,UITableViewDataSource>@end//// ViewController.m#import"ViewController.h"#import"CustomCell.h"@interfaceViewController()@end@implementationViewController-(void)viewDidLoad{[superviewDidLoad];self.view.backgroundColor=[UIColor whiteColor];UITableView*tableView=[[UITableView alloc]initWithFrame:self.view.bounds style:UITableViewStylePlain];tableView.dataSource=self;tableView.delegate=self;tableView.rowHeight=68;[self.view addSubview:tableView];[tableView registerClass:[CustomCell class]forCellReuseIdentifier:@"CustonCellID"];// 非注册方式// viewDidLoad 里不写任何东西// CustomCell 类名出现在这里,复用池没有时你自己 alloc init// CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CustomCell"];// if (cell == nil) {// cell = [[CustomCell alloc] initWithStyle:UITableViewCellStyleDefault// reuseIdentifier:@"CustomCell"]; // 你自己创建// }}-(NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section{return10;}-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{// 强制转换成自定义类型CustomCell*cell=[tableView dequeueReusableCellWithIdentifier:@"CustonCellID"forIndexPath:indexPath];cell.titleLabel.text=[NSString stringWithFormat:@"title:%ld",indexPath.row];// 右侧箭头/开关等系统控件cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;UIView*viewFirst=[[UIView alloc]init];viewFirst.backgroundColor=[UIColor yellowColor];// 正常状态背景cell.backgroundView=viewFirst;UIView*viewSelected=[[UIView alloc]init];viewSelected.backgroundColor=[UIColor lightGrayColor];// 选中时的背景cell.selectedBackgroundView=viewSelected;cell.subLabel.text=@"subtitle...";returncell;}@end自定义Cell的完整结构
UITableViewCell └── contentView ← 自定义内容加在这里 ├── swi ├── titleLabel └── subLabel//自定义的属性└── accessoryView ← 右侧箭头/开关等系统控件 └── backgroundView ← 背景 └── selectedBackgroundView ← 选中时的背景UITableViewCell 本质是一个可复用的视图容器,而不是与数据一一对应的对象
我们刚刚在CustomCell.h/.m写的就是contentView 部分,所以一定要注意改文件的控件要添加到self.contentView下而不是self.view
对于accessoryView,它是系统带的,也有很多类型
// 右侧箭头 >(最常见,表示可以点进去)cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;// 对勾 ✓(选中状态)cell.accessoryType=UITableViewCellAccessoryCheckmark;// 带箭头的 info 按钮cell.accessoryType=UITableViewCellAccessoryDetailDisclosureButton;// 无(默认)cell.accessoryType=UITableViewCellAccessoryNone;// 或者自定义视图// 放一个开关UISwitch*sw=[[UISwitch alloc]init];cell.accessoryView=sw;注意:contentView·的内容不要延伸到最右边 否则会被accessoryView挡住,因为在渲染的过程中是backgroundView / selectedBackgroundView --> contentView --> accessoryView
因此 contentView 的布局需要预留右侧空间,避免被 accessoryView 遮挡
两种自定义方式
- 所有种类写在一个 Cell 类里
// 一个 CustomCell.m 里放所有类型@implementationCustomCell-(instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString*)reuseIdentifier{self=[superinitWithStyle:style reuseIdentifier:reuseIdentifier];if(self){if([reuseIdentifier isEqualToString:@"ImageCell"]){[selfsetupImageStyle];// 图文样式自定义}elseif([reuseIdentifier isEqualToString:@"TextCell"]){[selfsetupTextStyle];// 纯文字样式自定义}elseif([reuseIdentifier isEqualToString:@"VideoCell"]){[selfsetupVideoStyle];// 视频样式自定义}}returnself;}// viewDidLoad里使用// 三种 identifier 都注册同一个类[self.tableView registerClass:[CustomCell class]forCellReuseIdentifier:@"ImageCell"];[self.tableView registerClass:[CustomCell class]forCellReuseIdentifier:@"TextCell"];[self.tableView registerClass:[CustomCell class]forCellReuseIdentifier:@"VideoCell"];-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{if(indexPath.row%3==0){CustomCell*cell=[tableView dequeueReusableCellWithIdentifier:@"ImageCell"forIndexPath:indexPath];returncell;}elseif(indexPath.row%3==1){CustomCell*cell=[tableView dequeueReusableCellWithIdentifier:@"TextCell"forIndexPath:indexPath];returncell;}else{CustomCell*cell=[tableView dequeueReusableCellWithIdentifier:@"VideoCell"forIndexPath:indexPath];returncell;}}- 不同种类写在不同 Cell 类里
// ImageCell.h 专门负责图文@interfaceImageCell:UITableViewCell@property(nonatomic,strong)UIImageView*thumbView;@property(nonatomic,strong)UILabel*titleLabel;@end// TextCell.h 专门负责纯文字@interfaceTextCell:UITableViewCell@property(nonatomic,strong)UILabel*titleLabel;@property(nonatomic,strong)UILabel*contentLabel;@end// VideoCell.h 专门负责视频@interfaceVideoCell:UITableViewCell@property(nonatomic,strong)UIView*videoPlayer;@property(nonatomic,strong)UILabel*durationLabel;@end// ViewController 里注册和使用// 每个类注册自己的 identifier[self.tableView registerClass:[ImageCell class]forCellReuseIdentifier:@"ImageCell"];[self.tableView registerClass:[TextCell class]forCellReuseIdentifier:@"TextCell"];[self.tableView registerClass:[VideoCell class]forCellReuseIdentifier:@"VideoCell"];-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath{if(indexPath.row%3==0){ImageCell*cell=[tableView dequeueReusableCellWithIdentifier:@"ImageCell"forIndexPath:indexPath];cell.titleLabel.text=@"图文标题";returncell;}elseif(indexPath.row%3==1){TextCell*cell=[tableView dequeueReusableCellWithIdentifier:@"TextCell"forIndexPath:indexPath];cell.contentLabel.text=@"正文内容";returncell;}else{VideoCell*cell=[tableView dequeueReusableCellWithIdentifier:@"VideoCell"forIndexPath:indexPath];cell.durationLabel.text=@"03:45";returncell;}}一般我们都会推荐使用第二种,第一种所有代码都堆在一起维护麻烦,第二种就职责清晰,互不影响
