iOS开发基础19-深入理解和实现不等高的 UITableViewCell

在 iOS 开发中,UITableView 是一个非常常用的组件,用于展示大量数据。同时,很多时候我们希望每个 UITableViewCell(简称 Cell)的高度能够根据内容自动调整。这篇文章将深入探讨如何使用纯代码自定义不等高的 Cell,并进行优化和性能分析。

主要步骤

  1. 创建自定义的 UITableViewCell 及其数据模型。
  2. 在控制器中加载数据,计算每个 Cell 的高度。
  3. 使用 heightForRowAtIndexPath: 方法返回每个 Cell 的实际高度。

实现代码

1. 创建自定义 UITableViewCell 和数据模型

首先,我们需要创建一个继承自 UITableViewCell 的类,例如 XMGStatusCell,并在其初始化方法中添加所有需要的子控件。

自定义 UITableViewCell

#import <UIKit/UIKit.h>

@interface XMGStatusCell : UITableViewCell

@property (nonatomic, strong) UIImageView *iconImageView;
@property (nonatomic, strong) UILabel *nameLabel;
@property (nonatomic, strong) UILabel *textLabel;
@property (nonatomic, strong) UIImageView *pictureImageView;

@end

#import "XMGStatusCell.h"

@implementation XMGStatusCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self setupSubviews];
    }
    return self;
}

- (void)setupSubviews {
    self.iconImageView = [[UIImageView alloc] init];
    self.nameLabel = [[UILabel alloc] init];
    self.textLabel = [[UILabel alloc] init];
    self.pictureImageView = [[UIImageView alloc] init];
    
    [self.contentView addSubview:self.iconImageView];
    [self.contentView addSubview:self.nameLabel];
    [self.contentView addSubview:self.textLabel];
    [self.contentView addSubview:self.pictureImageView];
}

@end

自定义数据模型 XMGStatus

接下来,创建一个数据模型类 XMGStatus,包含所有需要的属性,如名字、文本、头像和配图等,并计算每个字段的 frame 以及 Cell 的总高度。

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

@interface XMGStatus : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *text;
@property (nonatomic, copy) NSString *icon;
@property (nonatomic, copy) NSString *picture;
@property (nonatomic, assign) BOOL vip;

// Frame 数据
@property (nonatomic, assign) CGRect iconFrame;
@property (nonatomic, assign) CGRect nameFrame;
@property (nonatomic, assign) CGRect textFrame;
@property (nonatomic, assign) CGRect pictureFrame;
@property (nonatomic, assign) CGFloat cellHeight;

@end

#import "XMGStatus.h"

@implementation XMGStatus

- (CGFloat)cellHeight {
    if (_cellHeight == 0) {
        CGFloat margin = 10;

        // 头像
        CGFloat iconX = margin;
        CGFloat iconY = margin;
        CGFloat iconWH = 30;
        self.iconFrame = CGRectMake(iconX, iconY, iconWH, iconWH);

        // 昵称(姓名)
        CGFloat nameX = CGRectGetMaxX(self.iconFrame) + margin;
        CGFloat nameY = iconY;
        NSDictionary *nameAttributes = @{NSFontAttributeName : [UIFont systemFontOfSize:17]};
        CGSize nameSize = [self.name sizeWithAttributes:nameAttributes];
        self.nameFrame = (CGRect){{nameX, nameY}, nameSize};

        // 文字
        CGFloat textX = iconX;
        CGFloat textY = CGRectGetMaxY(self.iconFrame) + margin;
        CGFloat textWidth = [UIScreen mainScreen].bounds.size.width - 2 * textX;
        CGSize textMaxSize = CGSizeMake(textWidth, MAXFLOAT);
        NSDictionary *textAttributes = @{NSFontAttributeName : [UIFont systemFontOfSize:14]};
        CGFloat textHeight = [self.text boundingRectWithSize:textMaxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:textAttributes context:nil].size.height;
        self.textFrame = CGRectMake(textX, textY, textWidth, textHeight);

        // 配图
        if (self.picture) {
            CGFloat pictureWH = 100;
            CGFloat pictureX = textX;
            CGFloat pictureY = CGRectGetMaxY(self.textFrame) + margin;
            self.pictureFrame = CGRectMake(pictureX, pictureY, pictureWH, pictureWH);
            _cellHeight = CGRectGetMaxY(self.pictureFrame) + margin;
        } else {
            _cellHeight = CGRectGetMaxY(self.textFrame) + margin;
        }
    }
    return _cellHeight;
}

@end

2. 设置数据和布局子控件

为不等高的 UITableViewCell 设置布局和数据是非常关键的一步。确保所有子控件的位置和大小都是基于模型中的 frame 进行动态计算的。

@implementation XMGStatusCell

- (void)setStatus:(XMGStatus *)status {
    _status = status;
    
    self.iconImageView.image = [UIImage imageNamed:status.icon];
    self.iconImageView.frame = status.iconFrame;
    
    self.nameLabel.text = status.name;
    self.nameLabel.frame = status.nameFrame;
    
    self.textLabel.text = status.text;
    self.textLabel.frame = status.textFrame;
    
    if (status.picture) {
        self.pictureImageView.hidden = NO;
        self.pictureImageView.image = [UIImage imageNamed:status.picture];
        self.pictureImageView.frame = status.pictureFrame;
    } else {
        self.pictureImageView.hidden = YES;
    }
}

- (void)layoutSubviews {
    [super layoutSubviews];
    
    self.iconImageView.frame = self.status.iconFrame;
    self.nameLabel.frame = self.status.nameFrame;
    self.textLabel.frame = self.status.textFrame;
    self.pictureImageView.frame = self.status.pictureFrame;
}

@end

3. 计算并返回 Cell 高度

UITableViewController 中,我们需要通过 heightForRowAtIndexPath: 方法来返回每个 Cell 的高度。在 Cell 的高度计算中,我们可以预先缓存高度。

#import "ViewController.h"
#import "XMGStatus.h"
#import "XMGStatusCell.h"
#import "MJExtension.h"

@interface ViewController ()

@property (nonatomic, strong) NSArray *statuses;

@end

@implementation ViewController

NSString *ID = @"status";

- (NSArray *)statuses {
    if (!_statuses) {
        _statuses = [XMGStatus objectArrayWithFilename:@"statuses.plist"];
    }
    return _statuses;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    // 告诉 tableView 所有 cell 的真实高度是自动计算(根据设置的约束来计算)
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    // 告诉 tableView 所有 cell 的估算高度
    self.tableView.estimatedRowHeight = 200;
}

#pragma mark - 数据源方法

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.statuses.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    XMGStatusCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    
    cell.status = self.statuses[indexPath.row];
    
    return cell;
}

#pragma mark - 代理方法

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    XMGStatus *status = self.statuses[indexPath.row];
    return status.cellHeight;
}

@end

底层逻辑解析与优化

自适应高度计算的底层逻辑

在iOS中,UITableView 的高度缓存机制背后强大而复杂。当调用 heightForRowAtIndexPath: 方法时,系统会在内部缓存高度,以便后续快速访问。而自适应的 UITableViewCell 通过设置 tableView.rowHeightUITableViewAutomaticDimensionestimatedRowHeight 的结合使用,使得系统能够自动计算并缓存高度。

优化方案

1. 使用预估高度减少计算

通过设置 tableView.estimatedRowHeight,我们可以减少 heightForRowAtIndexPath: 方法的频繁调用,从而提升性能。

self.tableView.estimatedRowHeight = 200;

2. 缓存 Cell 高度

提前在数据模型中计算并缓存好 Cell 高度,避免在 heightForRowAtIndexPath: 中重复计算。

3. 批量更新表格

使用 beginUpdatesendUpdates 方法包裹需要批量更新的表格操作,可以提升性能。例如,当需要插入或者删除多行时:

[self.tableView beginUpdates];
// 插入或者删除行
[self.tableView endUpdates];

4. 异步加载数据

对于需要从网络等耗时操作中加载数据时,可以优先加载简单的部分,然后异步加载图片等数据,提升用户体验。

性能测试与调优

我们可以使用 Instruments 中的 Time Profiler 工具来检测我们应用的性能瓶颈,看看哪些方法占用了较多的 CPU 时间,以及通过优化代码来减少高频繁方法调用。

结论

通过深入分析和优化,我们成功实现了一个不等高的 UITableViewCell。通过对子控件的动态布局、缓存 Cell 高度,以及性能优化等手段,可以在实际项目中更好地展示不等高的 Cell 效果。

posted @ 2015-07-23 23:21  Mr.陳  阅读(4139)  评论(1编辑  收藏  举报