纯代码自定义不等高cell
数据模型、plist解析这里就不过多赘述。
错误思路之一:
通过在heightForRowAtIndexPath:方法中调用cellForRowAtIndexPath:拿到cell,再拿到cell的子控件的最大Y值的方法是不可取的。会出现死循环,因为cellForRowAtIndexPath:方法中会调用heightForRowAtIndexPath:方法。
错误思路之二:
在layoutSubviews方法中,根据子控件的高度,计算cell的高度。先初始化一个变量为0为cell的高度,计算出来所有子控件的高度,最后再计算cell的高度。
思路2.1
在layoutSubViews中设置cell的高度,不可行,因为会继续调用layoutSubviews,(当cell的frame被修改时候会调用layoutSubViews)
思路2.2
再给自定义cell添加一个cellHeight属性,拿到在layoutSubviews中计算的cell的高度。再再heightForRowAtIndexPath:返回行高,也是不可行的,照样死循环(返回cell的高度需要先拿到cell,所以死循环)。
错误思路之三:
在模型中添加一个行高属性,把layoutSubviews中计算的cell高度赋值给模型的行高属性,在heightForRowAtIndexPath:中通过模型拿到行高,不可行。因为时机不对。事实上,系统会先调用heightForRowAtIndexPath:方法,然后计算出来cell的高度,根据cell的高度调用layoutSubviews方法,布局子控件的位置。
综合思路三:
要想拿到cell高度,需要在heightForRowAtIndexPath:方法调用之前将所有的子控件的高度计算清楚。
在heightForRowAtIndexPath:方法中再次计算(空算)子控件的高度,定义局部变量cellHeight,根据子控件的最大Y值,计算cellHeight,返回cellHeight
正确思路:
1.自定义UITableViewCell,继承自UITableViewCell
2.自定义UITableViewCell中重写- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
目的:给自定义cell添加子控件
2.1.注意:该方法中调用父类同名方法
2.2.该方法中为自定义cell创建子控件,并把这些子控件添加到自定义cell的contentView中
2.3.给自定义cell添加子控件属性,保住子控件的命,在其他方法中也能访问子控件
2.4.返回自定义cell
3.重写- (void)layoutSubviews
目的:布局子控件
3.1.注意:一定要调用[super layoutSubviews];
3.2.该方法中根据需求计算子控件的frame
4.给自定义cell添加数据模型属性,重写该属性的set方法
目的:在数据源方法- (UITableViewCell *)tableView:cellForRowAtIndexPath:中直接为cell的模型属性赋值,更加MVC
注意:该数据模型属性一定要声明在.h文件中,为了外界能够调用
4.1.重写自定义cell的模型属性的set方法
4.2.在set方法中给自定义cell的子控件赋值
4.3.根据实际情况判断子控件的显示和隐藏
5.实现UITableView的delegate方法- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
目的:在该方法中再次计算每个子控件的高度
5.1.根据实际情况(某些子控件的显示或者隐藏)确定cell的高度
5.2.返回计算出来的cell的高度
如下图,为项目的目录结构:
数据模型:
/***********************数据模型.h文件***********************/ #import <UIKit/UIKit.h> @interface WSBlogItem : UIView /** 名称 */ @property (nonatomic,copy) NSString *name; /** 内容 */ @property (nonatomic,copy) NSString *text; /** 头像 */ @property (nonatomic,copy) NSString *icon; /** 配图 */ @property (nonatomic,copy) NSString *picture; /** 是否会员 */ @property (nonatomic,assign,getter=isVip) BOOL vip; // 苹果规范:一般一个类工厂方法对应一个自定义构造方法 /** 自定义构造方法 (字典->模型)*/ - (instancetype)initWithDict:(NSDictionary *)dict; /** 类工厂方法 (字典->模型) */ + (instancetype)blogItemWithDict:(NSDictionary *)dict; @end
/***********************数据模型.m文件***********************/ #import "WSBlogItem.h" @implementation WSBlogItem - (instancetype)initWithDict:(NSDictionary *)dict { if (self = [super init]) { self.name = dict[@"name"]; self.text = dict[@"text"]; self.icon = dict[@"icon"]; self.picture = dict[@"picture"]; self.vip = dict[@"vip"]; } return self; } + (instancetype)blogItemWithDict:(NSDictionary *)dict { // 调用自定义构造方法 return [[self alloc] initWithDict:dict]; } @end
自定义cell:
/***********************自定义cell.h文件***********************/ #import <UIKit/UIKit.h> #import "WSBlogItem.h" @interface WSBlogCell : UITableViewCell
/** 自定义cell的数据模型属性 */ @property (nonatomic,strong) WSBlogItem *blogItem;
@end
/***********************自定义cell.m文件***********************/ #import "WSBlogCell.h" #define WSNameFont [UIFont systemFontOfSize:17] #define WSTextFont [UIFont systemFontOfSize:14] @interface WSBlogCell () /** 名称Label */ @property(nonatomic,strong) UILabel *WSNameLabel; /** 文本Label */ @property(nonatomic,strong) UILabel *WSTextLabel; /** 头像ImageView */ @property(nonatomic,strong) UIImageView *WSIconView; /** 配图ImageView */ @property(nonatomic,strong) UIImageView *WSPictureView; /** 是否vip的Label */ @property(nonatomic,strong) UIImageView *WSVipView; @end @implementation WSBlogCell #pragma mark - 重写initWithStyle:方法 (创建、添加子控件) - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { // 名称Label UILabel *WSNameLabel = [[UILabel alloc] init]; self.WSNameLabel = WSNameLabel; [self.contentView addSubview:WSNameLabel]; // 文本Label UILabel *WSTextLabel = [[UILabel alloc] init]; WSTextLabel.numberOfLines = 0; // 文字可以换行显示 WSTextLabel.font = WSTextFont; // 不设置字体大小会导致文字不能完全显示(即使numberOfLines = 0) self.WSTextLabel = WSTextLabel; [self.contentView addSubview:WSTextLabel]; // 是否vip的Label UIImageView *WSVipView = [[UIImageView alloc] init]; self.WSVipView = WSVipView; [self.contentView addSubview:WSVipView]; // 头像ImageView UIImageView *WSIconView = [[UIImageView alloc] init]; self.WSIconView = WSIconView; [self.contentView addSubview:WSIconView]; // 配图ImageView UIImageView *WSPictureView = [[UIImageView alloc] init]; self.WSPictureView = WSPictureView; [self.contentView addSubview:WSPictureView]; } return self; } #pragma mark - 布局子控件 - (void)layoutSubviews { // 1.千万不能漏掉这句 [super layoutSubviews]; // 2.空算所有的子控件的frame CGFloat margin = 10; // 头像 CGFloat iconX = margin; CGFloat iconY = margin; CGFloat iconWH = 30; CGRect iconFrame = CGRectMake(iconX, iconY, iconWH, iconWH); self.WSIconView.frame = iconFrame; // 昵称(姓名) CGFloat nameY = iconY; CGFloat nameX = CGRectGetMaxX(iconFrame) + margin; // 计算文字所占据的尺寸 NSDictionary *nameAttrs = @{NSFontAttributeName : [UIFont systemFontOfSize:17]}; CGSize nameSize = [self.WSNameLabel.text sizeWithAttributes:nameAttrs]; CGRect nameFrame = (CGRect){{nameX, nameY}, nameSize}; self.WSNameLabel.frame = nameFrame; // 会员图标 if (self.blogItem.isVip) { CGFloat vipW = 14; CGFloat vipH = nameSize.height; CGFloat vipY = nameY; CGFloat vipX = CGRectGetMaxX(nameFrame) + margin; CGRect vipFrame = CGRectMake(vipX, vipY, vipW, vipH); self.WSVipView.frame = vipFrame; } // 文字 CGFloat textX = iconX; CGFloat textY = CGRectGetMaxY(iconFrame) + margin; CGFloat textW = self.contentView.frame.size.width - 2 * textX; CGSize textMaxSize = CGSizeMake(textW, MAXFLOAT); NSDictionary *textAttrs = @{NSFontAttributeName : WSTextFont}; CGFloat textH = [self.blogItem.text boundingRectWithSize:textMaxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:textAttrs context:nil].size.height; CGRect textFrame = CGRectMake(textX, textY, textW, textH); self.WSTextLabel.frame = textFrame; // 配图 if (self.blogItem.picture) { CGFloat pictureWH = 100; CGFloat pictureX = textX; CGFloat pictureY = CGRectGetMaxY(textFrame) + margin; CGRect pictureFrame = CGRectMake(pictureX, pictureY, pictureWH, pictureWH); self.WSPictureView.frame = pictureFrame; } } #pragma mark - 重写blogItem属性的set方法(内部对cell的子控件进行赋值) - (void)setBlogItem:(WSBlogItem *)blogItem { _blogItem = blogItem; // 对cell的子控件赋值 self.WSNameLabel.text = blogItem.name; self.WSTextLabel.text = blogItem.text; self.WSIconView.image = [UIImage imageNamed:blogItem.icon]; if (blogItem.vip) { // name颜色为黄色 self.WSVipView.hidden = NO; self.WSNameLabel.textColor = [UIColor yellowColor]; }else{ // name颜色为黑色 self.WSVipView.hidden = YES; self.WSNameLabel.textColor = [UIColor blackColor]; } if (blogItem.picture) { // 有配图 self.WSPictureView.hidden = NO; self.WSPictureView.image = [UIImage imageNamed:blogItem.picture]; }else{ // 无配图 self.WSPictureView.hidden = YES; } } @end
控制器:
/***********************控制器.h文件***********************/ #import <UIKit/UIKit.h> @interface ViewController : UITableViewController @end
/***********************控制器.m文件***********************/ #import "ViewController.h" #import "WSBlogItem.h" #import "WSBlogCell.h" @interface ViewController () // 注意:懒加载的对象必须是strong /** 模型数组 */ @property(nonatomic,strong)NSArray *blogItems; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // tableView的行高由row自己决定,不是由cell决定 self.tableView.rowHeight = 200; } #pragma mark - datasource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.blogItems.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { // 重用标识 static NSString *ID = @"blogCell"; // 重用 WSBlogCell *cell = [self.tableView dequeueReusableCellWithIdentifier:ID]; // tableView的缓存池中没有可以重用的cell就创建并设置重用标识 if (!cell) { cell = [[WSBlogCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID]; NSLog(@"newCell"); } // 取出对应行的数据模型 WSBlogItem *blogItem = self.blogItems[indexPath.row]; // 调用cell的blogItem属性的set方法,给这个cell设置数据 cell.blogItem = blogItem; // 返回设置好数据的cell return cell; } #pragma mark - delegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { WSBlogItem *blogItem = self.blogItems[indexPath.row]; CGFloat margin = 10; // 头像 CGFloat iconX = margin; CGFloat iconY = margin; CGFloat iconWH = 30; CGRect iconFrame = CGRectMake(iconX, iconY, iconWH, iconWH); // self.WSIconView.frame = iconFrame; // 昵称(姓名) CGFloat nameY = iconY; CGFloat nameX = CGRectGetMaxX(iconFrame) + margin; // 计算文字所占据的尺寸 NSDictionary *nameAttrs = @{NSFontAttributeName : [UIFont systemFontOfSize:17]}; CGSize nameSize = [blogItem.text sizeWithAttributes:nameAttrs]; CGRect nameFrame = (CGRect){{nameX, nameY}, nameSize}; // 会员图标 if (blogItem.isVip) { CGFloat vipW = 14; CGFloat vipH = nameSize.height; CGFloat vipY = nameY; CGFloat vipX = CGRectGetMaxX(nameFrame) + margin; CGRect vipFrame = CGRectMake(vipX, vipY, vipW, vipH); } // 文字 CGFloat textX = iconX; CGFloat textY = CGRectGetMaxY(iconFrame) + margin; CGFloat textW = self.tableView.frame.size.width - 2 * textX; CGSize textMaxSize = CGSizeMake(textW, MAXFLOAT); NSDictionary *textAttrs = @{NSFontAttributeName : [UIFont systemFontOfSize:14]}; CGFloat textH = [blogItem.text boundingRectWithSize:textMaxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:textAttrs context:nil].size.height; CGRect textFrame = CGRectMake(textX, textY, textW, textH); // 配图 if (blogItem.picture) { CGFloat pictureWH = 100; CGFloat pictureX = textX; CGFloat pictureY = CGRectGetMaxY(textFrame) + margin; CGRect pictureFrame = CGRectMake(pictureX, pictureY, pictureWH, pictureWH); return CGRectGetMaxY(pictureFrame) + margin; }else{ return CGRectGetMaxY(textFrame) + margin; } } #pragma mark - lazyLoad - (NSArray *)blogItems { // 注意用 !_blogItems 的好处,相对于 _blogItems == nil if (!_blogItems) { // 注意不要用 self.blogItems,避免出现get方法循环调用 _blogItems = [NSArray array]; // 1.获取plist文件路径 NSString *path = [[NSBundle mainBundle] pathForResource:@"statuses.plist" ofType:nil]; // 2.加载plist NSArray *arrDict = [NSArray arrayWithContentsOfFile:path]; // 3.字典数组->模型数组 // 3.1.创建临时可变数组 NSMutableArray *arrM = [NSMutableArray array]; for (NSDictionary *dict in arrDict) { WSBlogItem *blogItem = [WSBlogItem blogItemWithDict:dict]; [arrM addObject:blogItem]; } // 3.2.可变数组->不可变数组(为了安全) // 注意arrM的作用范围,不要写到{}外面 _blogItems = [arrM copy]; } // 4.返回 return _blogItems; } @end
运行效果图:
优化:因为在heightForRowAtIndexPath:方法中和layoutSubviews方法中计算了两次子控件的frame,而系统先会
调用heightForRowAtIndexPath:,所以可以把heightForRowAtIndexPath:计算的子控件的frame(包括子
控件的高度)保存到模型中,在layoutSubviews中直接用模型中的子控的frame。不需要再次在layoutSubviews中计算子控件的
frame。
再次优化:
给模型添加cellHeight属性
在heightForRowAtIndexPath:中直接返回模型的cellHeight属性
在模型的.h文件中重写cellHeight属性的get方法,在get方法中计算子控件的frame(包括子控件的高度),最后返回cellHeight。
数据模型做了如下修改:
#import <UIKit/UIKit.h> @interface WSBlogItem : UIView /***********************文字/图片数据***********************/ /** 名称 */ @property (nonatomic,copy) NSString *name; /** 内容 */ @property (nonatomic,copy) NSString *text; /** 头像 */ @property (nonatomic,copy) NSString *icon; /** 配图 */ @property (nonatomic,copy) NSString *picture; /** 是否会员 */ @property (nonatomic,assign,getter=isVip) BOOL vip; /***********************frame数据***********************/ @property (nonatomic,assign) CGRect nameFrame; @property (nonatomic,assign) CGRect textFrame; @property (nonatomic,assign) CGRect iconFrame; @property (nonatomic,assign) CGRect pictureFrame; @property (nonatomic,assign) CGRect vipFrame;
/** cell的高度 */
@property (nonatomic,assign) CGFloat cellHeight;
/***********************自定义方法***********************/ // 苹果建议:一般一个类工厂方法对应一个自定义构造方法 /** 自定义构造方法 (字典->模型)*/ - (instancetype)initWithDict:(NSDictionary *)dict; /** 类工厂方法 (字典->模型) */ + (instancetype)blogItemWithDict:(NSDictionary *)dict; @end
#import "WSBlogItem.h" #define WSTextFont [UIFont systemFontOfSize:14] @implementation WSBlogItem - (instancetype)initWithDict:(NSDictionary *)dict { if (self = [super init]) { self.name = dict[@"name"]; self.text = dict[@"text"]; self.icon = dict[@"icon"]; self.picture = dict[@"picture"]; self.vip = dict[@"vip"]; } return self; } + (instancetype)blogItemWithDict:(NSDictionary *)dict { // 调用自定义构造方法 return [[self alloc] initWithDict:dict]; } #pragma mark - 重写cellHeight的get方法 - (CGFloat)cellHeight { if (_cellHeight == 0) { // 计算所有的子控件的frame和高度 CGFloat margin = 10; // 头像 CGFloat iconX = margin; CGFloat iconY = margin; CGFloat iconWH = 30; CGRect iconFrame = CGRectMake(iconX, iconY, iconWH, iconWH); self.iconFrame = iconFrame; // 昵称(姓名) CGFloat nameY = iconY; CGFloat nameX = CGRectGetMaxX(iconFrame) + margin; // 计算文字所占据的尺寸 NSDictionary *nameAttrs = @{NSFontAttributeName : [UIFont systemFontOfSize:17]}; CGSize nameSize = [self.name sizeWithAttributes:nameAttrs]; CGRect nameFrame = (CGRect){{nameX, nameY}, nameSize}; self.nameFrame = nameFrame; // 会员图标 if (self.isVip) { CGFloat vipW = 14; CGFloat vipH = nameSize.height; CGFloat vipY = nameY; CGFloat vipX = CGRectGetMaxX(nameFrame) + margin; CGRect vipFrame = CGRectMake(vipX, vipY, vipW, vipH); self.vipFrame = vipFrame; } // 文字 CGFloat textX = iconX; CGFloat textY = CGRectGetMaxY(iconFrame) + margin; CGFloat textW = [UIScreen mainScreen].bounds.size.width - 2 * textX; CGSize textMaxSize = CGSizeMake(textW, MAXFLOAT); NSDictionary *textAttrs = @{NSFontAttributeName : WSTextFont}; CGFloat textH = [self.text boundingRectWithSize:textMaxSize options:NSStringDrawingUsesLineFragmentOrigin attributes:textAttrs context:nil].size.height; CGRect textFrame = CGRectMake(textX, textY, textW, textH); self.textFrame = textFrame; // 配图 if (self.picture) {// 有配图 CGFloat pictureWH = 100; CGFloat pictureX = textX; CGFloat pictureY = CGRectGetMaxY(textFrame) + margin; CGRect pictureFrame = CGRectMake(pictureX, pictureY, pictureWH, pictureWH); self.pictureFrame = pictureFrame; // cell的真实高度参考图片的高度 _cellHeight = CGRectGetMaxY(pictureFrame) + margin; }else{// 无配图 // cell的真实高度参考文字的高度 _cellHeight = CGRectGetMaxY(textFrame) + margin; } } return _cellHeight; } @end
自定义cell.m文件做了如下修改:
#pragma mark - 布局子控件 - (void)layoutSubviews { // 1.千万不能漏掉这句 [super layoutSubviews]; self.WSNameLabel.frame = self.blogItem.nameFrame; self.WSTextLabel.frame = self.blogItem.textFrame; self.WSIconView.frame = self.blogItem.iconFrame; self.WSPictureView.frame = self.blogItem.pictureFrame; self.WSVipView.frame = self.blogItem.vipFrame; }
#pragma mark - 重写blogItem属性的set方法(内部对cell的子控件进行赋值)
- (void)setBlogItem:(WSBlogItem *)blogItem
{
_blogItem = blogItem;
// 对cell的子控件赋值
self.WSNameLabel.text = blogItem.name;
self.WSTextLabel.text = blogItem.text;
self.WSIconView.image = [UIImage imageNamed:blogItem.icon];
if (blogItem.vip) {
// name颜色为黄色
self.WSVipView.hidden = NO;
self.WSNameLabel.textColor = [UIColor redColor];
}else{
// name颜色为黑色
self.WSVipView.hidden = YES;
self.WSNameLabel.textColor = [UIColor blackColor];
}
if (blogItem.picture) {
// 有配图
self.WSPictureView.hidden = NO;
self.WSPictureView.image = [UIImage imageNamed:blogItem.picture];
}else{
// 无配图
self.WSPictureView.hidden = YES;
}
}
控制器.m文件做了如下修改:
#pragma mark - delegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { WSBlogItem *blogItem = self.blogItems[indexPath.row]; return blogItem.cellHeight; }
其实,可以把- (void)layoutSubviews布局子控件的工作交给cell的数据模型的set方法做,因为- (void)setBlogItem:(WSBlogItem *)blogItem在- (void)layoutSubviews之后调用,这样就省去了- (void)layoutSubviews方法的重写和调用
#pragma mark - 布局子控件 //- (void)layoutSubviews //{ // // 1.千万不能漏掉这句 // [super layoutSubviews]; // // self.WSNameLabel.frame = self.blogItem.nameFrame; // self.WSTextLabel.frame = self.blogItem.textFrame; // self.WSIconView.frame = self.blogItem.iconFrame; // self.WSPictureView.frame = self.blogItem.pictureFrame; // self.WSVipView.frame = self.blogItem.vipFrame; //} #pragma mark - 重写blogItem属性的set方法(内部对cell的子控件进行赋值) - (void)setBlogItem:(WSBlogItem *)blogItem { _blogItem = blogItem; // 对cell的子控件赋值 self.WSNameLabel.text = blogItem.name; self.WSTextLabel.text = blogItem.text; self.WSIconView.image = [UIImage imageNamed:blogItem.icon]; if (blogItem.vip) { // name颜色为黄色 self.WSVipView.hidden = NO; self.WSNameLabel.textColor = [UIColor redColor]; }else{ // name颜色为黑色 self.WSVipView.hidden = YES; self.WSNameLabel.textColor = [UIColor blackColor]; } if (blogItem.picture) { // 有配图 self.WSPictureView.hidden = NO; self.WSPictureView.image = [UIImage imageNamed:blogItem.picture]; }else{ // 无配图 self.WSPictureView.hidden = YES; } // 设置子控件的frame self.WSNameLabel.frame = self.blogItem.nameFrame; self.WSTextLabel.frame = self.blogItem.textFrame; self.WSIconView.frame = self.blogItem.iconFrame; self.WSPictureView.frame = self.blogItem.pictureFrame; self.WSVipView.frame = self.blogItem.vipFrame; } @end
方法调用顺序总结 2->1->3:
1.- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
该方法中因为要知道cell的高度,所以该方法返回之前会先调用第2个方法
该方法中因为要给cell设置数据,所以会调用cell的数据模型属性的set方法- (void)setBlogItem:(WSBlogItem *)blogItem
- (void)setBlogItem:(WSBlogItem *)blogItem中给cell的子控件属性设置
2.- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath 计算子控件的frame然后保存下来
该方法要返回cell的高度,所以会调用数据模型的cellHeight属性的get方法- (CGFloat)cellHeight
- (CGFloat)cellHeight中计算子控件的frame并计算cell的高度,然后返回_cellHeight
3.- (void)layoutSubviews
调用该方法时候,上面的2个方法已经调用完成,所以子控件frame已经有值