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. 创建与配置

  1. New File → User Interface → Empty(或直接创建 Cocoa Touch Class 时勾选 "Also create XIB file")。
  2. 拖入一个 UITableViewCell,在 Identity Inspector 中将 Class 设为 CHGTgCell。
  3. 拖入子控件并设置 Auto Layout 约束。
  4. 将子控件连线到 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 中做额外配置。

posted @ 2015-07-20 23:48  Mr.陳  阅读(542)  评论(0)    收藏  举报