当前位置: 首页 > news >正文

【OC】自定义Cell

自定义Cell

文章目录

  • 自定义Cell
    • Cell复用
      • UITableView创建方式
      • Cell复用原理
    • 自定义Cell
      • 两种自定义方式

在 iOS 开发中,列表界面几乎无处不在,而UITableView是实现列表最常用的组件,系统为我们提供了默认的UITableViewCell,可以快速搭建简单的列表样式,但在实际开发中,这些默认样式往往难以满足复杂的 UI 需求,例如,一个常见的列表项可能包含头像、标题、副标题、时间、按钮等多个元素,并且还需要控制它们之间的间距和布局

因此,我们通常需要通过**“自定义 Cell”**的方式,来构建更加灵活、可扩展的列表项界面

在开始之前,我们先来简单复习一下UITableView的相关知识

Cell复用

UITableView继承于UIScrollView,拥有两个相关协议UITableViewDelegateUITableViewDataSource

UITableView是表,UITableViewCell是行,dataSource提供数据,**delegate **处理交互

UITableView创建方式

在使用 UITableView 时,创建 Cell 主要有两种方式:非注册方式注册方式

它们的核心区别在于Cell 的创建和复用机制是否由系统统一管理

  1. 非注册方式

非注册方式是较早期常见的写法,需要开发者手动判断 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;}
  1. 注册方式

先在初始化时注册 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,才会创建新的实例

需要注意的是:

  1. 当前屏幕上正在显示的 Cell,并不属于复用队列的一部分
  2. UITableView 不会缓存所有 Cell,而是按需创建和复用
  3. 数据的管理由 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 遮挡

两种自定义方式

  1. 所有种类写在一个 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;}}
  1. 不同种类写在不同 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;}}

一般我们都会推荐使用第二种,第一种所有代码都堆在一起维护麻烦,第二种就职责清晰,互不影响

http://www.jsqmd.com/news/775856/

相关文章:

  • 武汉明德智学高中课后辅导口碑如何 - myqiye
  • DeepSeek免费API逆向工程:技术原理、部署与实战应用
  • BabelDOC:专业PDF智能翻译工具的5分钟终极指南
  • 动态化漏洞利用框架:自动化适配与运行时决策机制解析
  • 类似龙虾企业级OpenClaw安全替代方案推荐:支持私有化部署的智能体平台 - 品牌2026
  • ThinkPad风扇控制终极指南:用TPFanCtrl2实现智能散热与静音平衡
  • 5倍效率跃迁:智能投递系统的数据驱动求职革命
  • 2026年新疆游骏文旅旅游人才吸引力排名 - myqiye
  • 猫抓终极指南:构建专业级浏览器资源嗅探与流媒体处理系统
  • Java 21 开发技术:简化数据流处理的模式匹配新探索
  • B站视频转文字:用bili2text轻松搞定内容提取难题
  • 3分钟解锁网易云音乐NCM加密文件:Windows图形化工具终极指南
  • 2026年南京办公设备厂家口碑推荐榜:南京打印机、南京复印机、南京印刷机、南京扫描仪、办公设备厂家选择指南 - 海棠依旧大
  • 2026年口碑好的龙井茶场有哪些? - mypinpai
  • Autobuy-JD:京东自动抢购工具完整指南与实战教程
  • 企业内如何通过Taotoken实现不同部门AI调用权限与配额管理
  • Claude API 无缝兼容 ChatGPT:一站式部署与配置指南
  • Cowabunga Lite终极指南:无需越狱的iOS个性化定制完全教程
  • 数据库性能提升10倍:SQL优化与索引策略实战解析
  • 如何解锁NVIDIA显卡隐藏设置:5个步骤掌握Profile Inspector
  • 基于AI智能体与Markdown文件构建可自我进化的第二大脑系统
  • 2026年代理记账靠谱公司哪家好 - mypinpai
  • SwarmClaw:自托管AI代理编排平台,构建多代理协作工作流
  • 2026年昆山装修公司零增项有哪些推荐 靠谱整装品牌避坑指南 - 速递信息
  • 5分钟部署手机号码归属地定位系统:location-to-phone-number完全实战指南
  • 基于Nuxt.js构建全栈ChatGPT应用:架构设计与核心实现
  • 如何在Ubuntu 26.04、24.04和22.04上安装NVIDIA驱动程序
  • 纠偏控制系统的参数调试技巧与优化方法
  • 2026年硅酸钙板生产厂好用排名 - mypinpai
  • Glowby OSS:本地优先AI编码代理工作流,开源赋能开发者