GJ's Blog

Talk is cheap.Show me the code.

AutoLayout实战:cell高度不固定的UITableView

| Comments

在没有AutoLayout之前,自定义一个高度不固定的cell是相当麻烦的。你需要写非常多计算尺寸的代码,在拿到数据后,需要计算cell里面每一个控件的尺寸才能最终确定cell的高度。如果你已经受够了各种计算尺寸的代码。那么本篇文章或许会对你有一些帮助,本文会说明如何利用AutoLayout优雅的实现具有动态高度的cell

在开始之前,先看下效果图,知道将要完成神马东西。

  • 高度随着内容变化,内容越多,高度就越高
  • 内容label,就是显示天气真好的label,最多显示3行

自定义Cell,继承自UITableViewCell

1
2
3
4
5
6
7
@interface GJCell : UITableViewCell

@property (nonatomic, weak) UIImageView *customImageView;
@property (nonatomic, weak) UILabel *title;
@property (nonatomic, weak) UILabel *subtitle;

@end
  • customImageView用于显示头像
  • title用于显示昵称
  • subtitle用于显示内容

创建UITableView并准备数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#import "GJCell.h"
#import "Masonry.h"

static NSString * const GJCellIndentifier = @"GJCell";

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>

@property (nonatomic, strong) NSArray *datas;
@property (nonatomic, weak) UITableView *tableView;

@end

@implementation ViewController

#pragma mark - Life Cycle

- (void)viewDidLoad {
    [super viewDidLoad];

    [self setupData];

    [self setupView];
}

- (void)setupData {
    NSDictionary *data1 = @{@"icon": @"myIcon",
                            @"name": @"GJBlog",
                            @"content": @"今天天气真好啊"};

    NSDictionary *data2 = @{@"icon": @"myIcon",
                            @"name": @"GJBlogGJBlogGJBlog",
                            @"content": @"今天天气真好啊今天天气真好啊今天天气真好啊今天天气真好啊"};

    NSDictionary *data3 = @{@"icon": @"myIcon",
                            @"name": @"GJBlogGJBlogGJBlogGJBlogGJBlog",
                            @"content": @"今天天气真好啊今天天气真好啊今天天气真好啊今天天气真好啊今天天气真好啊今天天气真好啊今天天气真好啊"};

    self.datas = @[data1, data2, data3];
}

- (void)setupView {
    UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
    [tableView registerClass:[GJCell class] forCellReuseIdentifier:GJCellIndentifier];
    tableView.dataSource = self;
    tableView.delegate = self;
    [self.view addSubview:tableView];
    self.tableView = tableView;

    [tableView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.view);
    }];
}
  • AutoLayout代码采用Masonry, 系统的那套太繁琐了.
  • tableView的大小等于self.view的大小.
  • self.datas里面有三条数据,每条数据的namecontent长度都是不一样的.尽量模拟真实情况.icon的值是一张本地图片的名称.

实现GJCell.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#import "GJCell.h"
#import "Masonry.h"

@implementation GJCell

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

        [self setupConstraint];
    }
    return self;
}

- (void)setupView {

    UIImageView *customImageView = [[UIImageView alloc] init];
    customImageView.layer.cornerRadius = 15.0f;
    customImageView.layer.masksToBounds = YES;
    [self.contentView addSubview:customImageView];
    _customImageView = customImageView;

    // 重点1
    CGFloat preferredWidth = [UIScreen mainScreen].bounds.size.width - 75;

    UILabel *title = [[UILabel alloc] init];
    title.numberOfLines = 0;
    // 重点1
    title.preferredMaxLayoutWidth = preferredWidth;
    title.textColor = [UIColor grayColor];
    [self.contentView addSubview:title];
    _title = title;

    UILabel *subtitle = [[UILabel alloc] init];
    subtitle.numberOfLines = 3;
    // 重点1
    subtitle.preferredMaxLayoutWidth = preferredWidth;
    [self.contentView addSubview:subtitle];
    _subtitle = subtitle;
}

- (void)setupConstraint {
    [self.customImageView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.contentView).with.offset(15.0f);
        make.right.equalTo(self.title.mas_left).with.offset(-15.0f);
        make.left.equalTo(self.contentView).with.offset(15.0f);
        make.size.mas_equalTo(CGSizeMake(30.0f, 30.0f));
    }];

    [self.title mas_makeConstraints:^(MASConstraintMaker *make) {
        // 重点2
        make.top.equalTo(self.contentView).with.offset(20.0f).with.priority(751);
        make.right.equalTo(self.contentView).with.offset(-15.0f);
    }];

    [self.subtitle mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.title.mas_bottom).with.offset(15.0f);
        make.right.equalTo(self.title);
        make.bottom.equalTo(self.contentView).with.offset(-15.0f).with.priority(749);
        make.left.equalTo(self.title);
    }];
}

@end

1.在setupView方法里面添加自定义控件

  • 设置customImageView为圆角, title的行数不限制, subtitle的行数最多为3行.

2.在setupConstraint方法里面添加自定义控件的约束

  • customImageView顶部、左边距离父视图15.0f, customImageView的右边与title的左边相距15.0f, customImageView的高度和宽度都等于30.0f.
  • title的顶部距离父视图20.0f, title的右边距离父视图15.0f.
  • subtitle的顶部与title的底部相距15.0f, subtitle的左边等于title的左边, subtitle的右边等于title的右边, subtitle的底部距离父视图15.0f.

3.说下代码中的两个重点

  • 重点1:告诉AutoLayout系统``label的最大宽度,便于计算高度。如果实在无法理解,等会实现tableview数据源代理之后,可以尝试注释掉重点1处的代码,然后看下效果。
  • 重点2:注意力放到with.priority(751)这里,两个label相邻,label的高度都是暂时无法确定,需要告诉AutoLayout系统约束的优先级,遇到无法同时满足约束时的优先满足级别。如果实在无法理解,同重点1,代码完善后,删掉with.priority(751),观察下控制台的输出。

在控制器中实现UItableViewDataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma mark - UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 60;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    GJCell *cell = [tableView dequeueReusableCellWithIdentifier:GJCellIndentifier forIndexPath:indexPath];
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

- (void)configureCell:(GJCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    NSInteger row = indexPath.row % 3;
    NSDictionary *data = self.datas[row];

    UIImage *image = [UIImage imageNamed:data[@"icon"]];
    [cell.customImageView setImage:image];

    [cell.title setText:data[@"name"]];

    [cell.subtitle setText:data[@"content"]];
}

实现UITableViewDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#pragma mark - UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return [self heightForCellAtIndexPath:indexPath];
}

- (CGFloat)heightForCellAtIndexPath:(NSIndexPath *)indexPath {
    static GJCell *cell = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        cell = [self.tableView dequeueReusableCellWithIdentifier:GJCellIndentifier];
    });
    [self configureCell:cell atIndexPath:indexPath];
    return [self calculateHeightForCell:cell];
}

- (CGFloat)calculateHeightForCell:(GJCell *)cell {
    [cell setNeedsLayout];
    [cell layoutIfNeeded];
    CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
    return size.height + 1.0f;
}

方法说明

  • heightForCellAtIndexPath:拿出一个cell用于计算,使用dispatch_once保证只执行一次,方法configureCell:atIndexPath:在数据源那块已经实现了,直接调用即可。
  • 调用calculateHeightForCell:计算cell的高度,先调用setNeedsLayoutlayoutIfNeeded让cell去布局子视图,然后调用systemLayoutSizeFittingSize:AutoLayout系统去计算大小, 参数UILayoutFittingCompressedSize的意思是告诉AutoLayout系统使用尽可能小的尺寸以满足约束,返回的结果里+1.0f是分割线的高度。

OK, 到这里基本结束了,可以编译运行了。

小小的优化一下

如果你的项目最低支持iOS7,那么你可以在UITableViewDelegate那块添加如下方法:

1
2
3
4
5
#pragma mark - UITableViewDelegate

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 112.0f;
}

实现该方法后,tableview就不会一次性调用完所有cell的高度,有些不在可见范围的cell是不需要一开始就知道高度的。当然,estimatedHeightForRowAtIndexPath方法调用频率就会非常高,所以我们尽量返回一个比较接近实际结果的固定值以提高性能.

Done!

如果你有任何评论或者问题,请在下面留言. I like it!
原创文章,转载请注明出处,谢谢!

Comments