iOS开发基础13-深入理解 UITableView(二)
自定义 UITableViewCell 全攻略:纯代码、Xib、多类型共存与模型转换
自定义 UITableViewCell 和字典转模型是列表开发的核心技能。本文涵盖纯代码与 Xib 两种自定义方式、Masonry/SnapKit 自动布局、MJExtension/Codable 模型转换、多类型 Cell 共存、Storyboard 原型 Cell 以及分割线与静态 Cell。
一、纯代码自定义等高 Cell
以一个团购列表为例,创建 CHGTgCell,包含图片、标题、价格和购买人数。
1. 添加子控件
Objective-C
// CHGTgCell.h
@interface CHGTgCell : UITableViewCell
@property (nonatomic, strong) UIImageView *iconImageView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *priceLabel;
@property (nonatomic, strong) UILabel *buyCountLabel;
@property (nonatomic, strong) CHGTg *tg;
+ (instancetype)cellWithTableView:(UITableView *)tableView;
@end
// CHGTgCell.m
@implementation CHGTgCell
+ (instancetype)cellWithTableView:(UITableView *)tableView {
static NSString *ID = @"tgCell";
CHGTgCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
if (!cell) {
cell = [[CHGTgCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
}
return cell;
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.iconImageView = [[UIImageView alloc] init];
self.iconImageView.contentMode = UIViewContentModeScaleAspectFill;
self.iconImageView.clipsToBounds = YES;
[self.contentView addSubview:self.iconImageView];
self.titleLabel = [[UILabel alloc] init];
self.titleLabel.font = [UIFont systemFontOfSize:16];
[self.contentView addSubview:self.titleLabel];
self.priceLabel = [[UILabel alloc] init];
self.priceLabel.font = [UIFont systemFontOfSize:14];
self.priceLabel.textColor = [UIColor redColor];
[self.contentView addSubview:self.priceLabel];
self.buyCountLabel = [[UILabel alloc] init];
self.buyCountLabel.font = [UIFont systemFontOfSize:12];
self.buyCountLabel.textColor = [UIColor grayColor];
[self.contentView addSubview:self.buyCountLabel];
}
return self;
}
Swift
class TgCell: UITableViewCell {
lazy var iconImageView: UIImageView = {
let iv = UIImageView()
iv.contentMode = .scaleAspectFill
iv.clipsToBounds = true
return iv
}()
lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 16)
return label
}()
lazy var priceLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 14)
label.textColor = .red
return label
}()
lazy var buyCountLabel: UILabel = {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
label.textColor = .gray
return label
}()
var tg: Tg? {
didSet {
iconImageView.image = UIImage(named: tg?.icon ?? "")
titleLabel.text = tg?.title
priceLabel.text = "¥\(tg?.price ?? "")"
buyCountLabel.text = "\(tg?.buyCount ?? "")人已购买"
}
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
contentView.addSubview(iconImageView)
contentView.addSubview(titleLabel)
contentView.addSubview(priceLabel)
contentView.addSubview(buyCountLabel)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
UITableViewCell的指定初始化方法是initWithStyle:reuseIdentifier:,它不会调用initWithFrame:。因此纯代码自定义 Cell 必须重写前者。
2. 手动 frame 布局
在 layoutSubviews 中计算所有子控件的 frame:
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat margin = 10;
CGFloat imageWidth = 60, imageHeight = 60;
self.iconImageView.frame = CGRectMake(margin, margin, imageWidth, imageHeight);
CGFloat titleX = CGRectGetMaxX(self.iconImageView.frame) + margin;
CGFloat titleWidth = self.contentView.bounds.size.width - titleX - margin;
self.titleLabel.frame = CGRectMake(titleX, margin, titleWidth, 20);
self.priceLabel.frame = CGRectMake(titleX, CGRectGetMaxY(self.titleLabel.frame) + margin, titleWidth, 20);
self.buyCountLabel.frame = CGRectMake(titleX, CGRectGetMaxY(self.priceLabel.frame) + margin, titleWidth, 20);
}
布局时使用
contentView.bounds(视图内部坐标系),而非frame。
3. 数据模型与绑定
// CHGTg.h
@interface CHGTg : NSObject
@property (nonatomic, copy) NSString *icon;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *price;
@property (nonatomic, copy) NSString *buyCount;
+ (instancetype)tgWithDict:(NSDictionary *)dict;
@end
// CHGTg.m
@implementation CHGTg
+ (instancetype)tgWithDict:(NSDictionary *)dict {
CHGTg *tg = [[self alloc] init];
tg.icon = dict[@"icon"];
tg.title = dict[@"title"];
tg.price = dict[@"price"];
tg.buyCount = dict[@"buyCount"];
return tg;
}
@end
// Cell 中重写 setter 绑定数据
- (void)setTg:(CHGTg *)tg {
_tg = tg;
self.iconImageView.image = [UIImage imageNamed:tg.icon];
self.titleLabel.text = tg.title;
self.priceLabel.text = [NSString stringWithFormat:@"¥%@", tg.price];
self.buyCountLabel.text = [NSString stringWithFormat:@"%@人已购买", tg.buyCount];
}
- (void)prepareForReuse {
[super prepareForReuse];
self.iconImageView.image = nil;
self.titleLabel.text = nil;
self.priceLabel.text = nil;
self.buyCountLabel.text = nil;
}
二、自动布局:Masonry / SnapKit
手动 frame 布局繁琐且不易维护,推荐使用 Masonry(OC)或 SnapKit(Swift)链式语法。
Objective-C(Masonry)
#import <Masonry/Masonry.h>
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
// ... 创建子控件(同上)
[self.iconImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.equalTo(self.contentView).offset(10);
make.size.mas_equalTo(CGSizeMake(60, 60));
}];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.iconImageView);
make.left.equalTo(self.iconImageView.mas_right).offset(10);
make.right.equalTo(self.contentView).offset(-10);
}];
[self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.titleLabel.mas_bottom).offset(10);
make.left.right.equalTo(self.titleLabel);
}];
[self.buyCountLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.priceLabel.mas_bottom).offset(10);
make.left.right.equalTo(self.titleLabel);
}];
}
return self;
}
Swift(SnapKit)
import SnapKit
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
// ... 添加子视图
iconImageView.snp.makeConstraints { make in
make.top.left.equalToSuperview().offset(10)
make.size.equalTo(CGSize(width: 60, height: 60))
}
titleLabel.snp.makeConstraints { make in
make.top.equalTo(iconImageView)
make.left.equalTo(iconImageView.snp.right).offset(10)
make.right.equalToSuperview().offset(-10)
}
priceLabel.snp.makeConstraints { make in
make.top.equalTo(titleLabel.snp.bottom).offset(10)
make.left.right.equalTo(titleLabel)
}
buyCountLabel.snp.makeConstraints { make in
make.top.equalTo(priceLabel.snp.bottom).offset(10)
make.left.right.equalTo(titleLabel)
}
}
等高 Cell 中底部不需要闭合约束(因为高度由
rowHeight固定);不等高自动计算高度时,必须有视图约束到contentView.bottomAnchor。
三、字典转模型
1. 手动遍历
最基础的方式,适用于简单数据结构:
- (NSArray<CHGTg *> *)tgs {
if (!_tgs) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"tgs" ofType:@"plist"];
NSArray *dictArray = [NSArray arrayWithContentsOfFile:path];
NSMutableArray *models = [NSMutableArray array];
for (NSDictionary *dict in dictArray) {
[models addObject:[CHGTg tgWithDict:dict]];
}
_tgs = models;
}
return _tgs;
}
2. MJExtension(OC 第三方库)
MJExtension 自动完成字典到模型的转换,支持嵌套模型:
#import "MJExtension.h"
// 从 plist 文件直接加载模型数组
_tgs = [CHGTg objectArrayWithFilename:@"tgs.plist"];
// 或从字典数组转换
_tgs = [CHGTg objectArrayWithKeyValuesArray:dictArray];
嵌套模型:当模型中包含另一个模型数组时,需声明容器类:
// CarGroup.h
@interface CarGroup : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSArray<Car *> *cars;
@end
// 使用时
[CarGroup setupObjectClassInArray:^NSDictionary *{
return @{@"cars": @"Car"}; // cars 数组中的元素解析为 Car 模型
}];
NSArray *groups = [CarGroup objectArrayWithFilename:@"cars.plist"];
3. Swift:Codable 协议
Swift 原生支持 Codable,无需第三方库即可完成 JSON/Plist 与模型的转换:
struct Tg: Codable {
let icon: String
let title: String
let price: String
let buyCount: String
}
// 从 plist 加载
let url = Bundle.main.url(forResource: "tgs", withExtension: "plist")!
let data = try! Data(contentsOf: url)
let tgs = try! PropertyListDecoder().decode([Tg].self, from: data)
// 从 JSON 加载
let jsonData = "...".data(using: .utf8)!
let tgs = try! JSONDecoder().decode([Tg].self, from: jsonData)
Codable要求属性名与 key 完全匹配,不一致时需实现CodingKeys枚举进行映射。
四、Xib 自定义等高 Cell
Xib 方式可视化设计 Cell,适合布局固定、样式明确的场景。
1. 创建与配置
New File → User Interface → Empty(或直接创建Cocoa Touch Class时勾选 "Also create XIB file")。- 拖入一个
UITableViewCell,在 Identity Inspector 中将 Class 设为CHGTgCell。 - 拖入子控件并设置 Auto Layout 约束。
- 将子控件连线到
CHGTgCell的IBOutlet。
2. 代码实现
Objective-C
// CHGTgCell.h
@interface CHGTgCell : UITableViewCell
@property (weak, nonatomic) IBOutlet UIImageView *iconImageView;
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UILabel *priceLabel;
@property (weak, nonatomic) IBOutlet UILabel *buyCountLabel;
@property (nonatomic, strong) CHGTg *tg;
@end
// CHGTgCell.m
@implementation CHGTgCell
- (void)awakeFromNib {
[super awakeFromNib];
// Xib 加载完成、连线建立后调用,适合做额外配置
self.iconImageView.contentMode = UIViewContentModeScaleAspectFill;
self.iconImageView.clipsToBounds = YES;
}
- (void)setTg:(CHGTg *)tg {
_tg = tg;
self.iconImageView.image = [UIImage imageNamed:tg.icon];
self.titleLabel.text = tg.title;
self.priceLabel.text = [NSString stringWithFormat:@"¥%@", tg.price];
self.buyCountLabel.text = [NSString stringWithFormat:@"%@人已购买", tg.buyCount];
}
@end
Swift
class TgCell: UITableViewCell {
@IBOutlet weak var iconImageView: UIImageView!
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var priceLabel: UILabel!
@IBOutlet weak var buyCountLabel: UILabel!
var tg: Tg? {
didSet {
iconImageView.image = UIImage(named: tg?.icon ?? "")
titleLabel.text = tg?.title
priceLabel.text = "¥\(tg?.price ?? "")"
buyCountLabel.text = "\(tg?.buyCount ?? "")人已购买"
}
}
override func awakeFromNib() {
super.awakeFromNib()
iconImageView.contentMode = .scaleAspectFill
iconImageView.clipsToBounds = true
}
}
3. 注册与使用
// 注册 Nib(Xib 编译后为 Nib)
[self.tableView registerNib:[UINib nibWithNibName:@"CHGTgCell" bundle:nil]
forCellReuseIdentifier:@"tgCell"];
// 使用
CHGTgCell *cell = [tableView dequeueReusableCellWithIdentifier:@"tgCell" forIndexPath:indexPath];
cell.tg = self.tgs[indexPath.row];
return cell;
registerNib:forCellReuseIdentifier:与registerClass:forCellReuseIdentifier:的区别:前者从 Xib 加载 Cell,后者通过代码创建。注册后dequeueReusableCellWithIdentifier:forIndexPath:保证返回非空。
五、不同类型的 Cell 共存
同一个 TableView 中可以有多种 Cell,通过不同的 reuseIdentifier 区分。
Objective-C
static NSString * const kTgCellID = @"tgCell";
static NSString * const kNewsCellID = @"newsCell";
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.rowHeight = 80;
// 注册两种 Cell
[self.tableView registerNib:[UINib nibWithNibName:@"CHGTgCell" bundle:nil] forCellReuseIdentifier:kTgCellID];
[self.tableView registerNib:[UINib nibWithNibName:@"CHGNewsCell" bundle:nil] forCellReuseIdentifier:kNewsCellID];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row % 2 == 0) {
CHGTgCell *cell = [tableView dequeueReusableCellWithIdentifier:kTgCellID forIndexPath:indexPath];
cell.tg = self.tgs[indexPath.row];
return cell;
} else {
CHGNewsCell *cell = [tableView dequeueReusableCellWithIdentifier:kNewsCellID forIndexPath:indexPath];
cell.news = self.news[indexPath.row];
return cell;
}
}
Swift
let tgCellID = "tgCell"
let newsCellID = "newsCell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = 80
tableView.register(UINib(nibName: "TgCell", bundle: nil), forCellReuseIdentifier: tgCellID)
tableView.register(UINib(nibName: "NewsCell", bundle: nil), forCellReuseIdentifier: newsCellID)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row % 2 == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: tgCellID, for: indexPath) as! TgCell
cell.tg = tgs[indexPath.row]
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: newsCellID, for: indexPath) as! NewsCell
cell.news = news[indexPath.row]
return cell
}
}
如果不同类型 Cell 高度不同,需实现
tableView:heightForRowAtIndexPath:或使用 Auto Layout 自动计算高度。
六、Storyboard 自定义 Cell
在 Storyboard 的 TableView 中直接拖入 Cell 原型(Prototype Cell),设置 Identifier 和 Custom Class。
特点
- 无需手动注册:Storyboard 中的原型 Cell 会自动注册,直接
dequeue即可。 - 可视化设计:直接在 Storyboard 中布局子控件、设置约束、连线 Outlet。
- 适合少量类型:原型 Cell 数量有限,复杂列表推荐代码或 Xib 方式。
使用
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row % 2 == 0) {
CHGTgCell *cell = [tableView dequeueReusableCellWithIdentifier:@"tgCell" forIndexPath:indexPath];
cell.tg = self.tgs[indexPath.row];
return cell;
} else {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"testCell" forIndexPath:indexPath];
return cell;
}
}
Storyboard 原型 Cell 必须使用
dequeueReusableCellWithIdentifier:forIndexPath:(带 indexPath),因为系统需要根据 indexPath 从原型创建 Cell。
七、分割线与静态 Cell
1. 系统分割线
UITableView 提供分割线相关属性:
| 属性 | 说明 |
|---|---|
separatorStyle |
分割线样式(None / SingleLine / SingleLineEtched) |
separatorColor |
分割线颜色 |
separatorInset |
分割线内边距(左右缩进) |
self.tableView.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
self.tableView.separatorColor = [UIColor lightGrayColor];
self.tableView.separatorInset = UIEdgeInsetsMake(0, 80, 0, 0); // 左侧缩进 80pt
2. 自定义分割线
系统分割线无法满足需求时,在 Cell 的 contentView 中手动添加:
- (void)setupSeparator {
UIView *separator = [[UIView alloc] init];
separator.backgroundColor = [UIColor colorWithWhite:0.9 alpha:1.0];
[self.contentView addSubview:separator];
[separator mas_makeConstraints:^(MASConstraintMaker *make) {
make.height.mas_equalTo(0.5); // 0.5pt 在 @2x 屏幕上为 1 像素
make.left.bottom.right.equalTo(self.contentView);
}];
}
分割线高度设为
0.5(点),在 @2x 屏幕上渲染为 1 像素,在 @3x 屏幕上渲染为 1.5 像素,是最细的视觉效果。
3. 静态 Cell
静态 Cell(Static Cells)在 Storyboard 中直接设计每一行的内容,不需要实现数据源方法(numberOfRowsInSection:、cellForRowAtIndexPath:),适合设置页、详情页等固定列表。
注意事项:
- 静态 Cell 只能在
UITableViewController中使用。 - 静态 Cell 不能使用
dequeueReusableCellWithIdentifier:forIndexPath:获取(会崩溃),应通过tableView:cellForRowAtIndexPath:获取已存在的 Cell。 - 动态修改静态 Cell 内容时,通过 Outlet 连线直接访问。
八、总结
| 自定义方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 纯代码 | 灵活、易于版本管理、适合复杂逻辑 | 代码量大、布局不直观 | 复杂交互、动态样式 |
| Xib | 可视化、代码量少、可复用 | 文件管理稍复杂 | 固定样式的通用 Cell |
| Storyboard 原型 | 最直观、无需注册 | 不可复用、不适合多类型 | 简单列表、少量 Cell |
| 静态 Cell | 零代码实现固定列表 | 仅支持 UITableViewController、不可动态 | 设置页、详情页 |
- 字典转模型:OC 推荐
MJExtension,Swift 推荐原生Codable。 - 自动布局:OC 用
Masonry,Swift 用SnapKit,或使用原生NSLayoutAnchor。 - 多类型 Cell 通过不同
reuseIdentifier区分,各自独立维护重用池。 - 自定义 Cell 务必重写
prepareForReuse重置状态,Xib Cell 在awakeFromNib中做额外配置。

浙公网安备 33010602011771号