1、cell中嵌套Tableview
实现思路
笔者尝试画了一张图,尽量反映出实现的思路:
外层UITableView通过HYBMasonryAutoCellHeight计算cell的高度并缓存起来,而cell中所嵌套的UITableView(显示评论信息的表格)也是通过HYBMasonryAutoCellHeight来计算cell的高度并缓存。当评论TableView增加或者删除一条数据时,通过代理反馈到外层TableView,然后重装计算行高并更新缓存。
数据建模
这里使用了两个模型类HYBTestModel是外层UITableView的数据源模型,而HYBCommentModel是评论UITableView的数据源模型。
HYBTestModel
@interface HYBTestModel : NSObject
@property (nonatomic,copy)NSString *uid;
@property(nonatomic,copy)NSString *title;
@property (nonatomic,copy)NSString *desc;
@property(nonatomic,copy)NSString *headImage;
// 评论数据源
@property (nonatomic,strong)NSMutableArray *commentModels;
// 因为评论是动态的,因此要标识是否要更新缓存
@property(nonatomic,assign)BOOL shouldUpdateCache;
@end
其中,uid必须保证唯一,它是用来缓存高度的唯一标识符,通常model的id作为uniqueKey。增加shouldUpdateCache属性的目的是在增加或者删除评论时,以识别是否需要重新计算行高并刷新对应的缓存高度。
HYBCommentModel
@interface HYBCommentModel: NSObject
@property (nonatomic,copy)NSString *cid;
@property(nonatomic,copy)NSString *name;
@property (nonatomic,copy)NSString *reply;
@property(nonatomic,copy)NSString *comment;
@end
这里的cid是指评论id,它是作为缓存行高的key。
外层HYBTestCell
这个是外层UITableView所需要使用的cell,它里面会嵌套着评论的UITableView。当评论内容发生变化时,通过代理来实现数据的刷新,行高的重新计算与缓存。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@class HYBTestModel;
@protocol HYBTestCellDelegate<NSObject>
- (void)reloadCellHeightForModel:(HYBTestModel*)model atIndexPath:(NSIndexPath*)indexPath;
@end
@interface HYBTestCell : UITableViewCell
@property (nonatomic,weak)id delegate;
- (void)configCellWithModel:(HYBTestModel*)model indexPath:(NSIndexPath*)indexPath;
@end
当我们配置其数据时,我们要计算出评论的tablview的高度。这里是通过HYBMasonryAutoCellHeight这个开源库来实现的。当配置数据时,通过遍历所有的评论模型,然后计算cell的行高,累加起来就是Tablview的高度,然后刷新tableview的约束。这样就可以正常地计算出整个外部cell的高度了。
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
- (void)configCellWithModel:(HYBTestModel*)model indexPath:(NSIndexPath*)indexPath{
self.indexPath= indexPath;
self.titleLabel.text= model.title;
self.descLabel.text= model.desc;
self.headImageView.image= [UIImage imageNamed:model.headImage];
self.testModel= model;
CGFloattableViewHeight = 0;
for(HYBCommentModel*commentModel in model.commentModels){
CGFloatcellHeight =[HYBCommentCell hyb_heightForTableView:self.tableView config:^(UITableViewCell*sourceCell){
HYBCommentCell*cell =(HYBCommentCell*)sourceCell;
[cell configCellWithModel:commentModel];
} cache:^NSDictionary*{
return@{kHYBCacheUniqueKey: commentModel.cid,
kHYBCacheStateKey: @"",
kHYBRecalculateForStateKey :@(NO)};
}];
tableViewHeight+= cellHeight;
}
[self.tableView mas_updateConstraints:^(MASConstraintMaker*make){
make.height.mas_equalTo(tableViewHeight);
}];
self.tableView.dataSource= self;
self.tableView.delegate= self;
[self.tableViewreloadData];
}
计算评论cell的高度
因为评论cell的内容不会改变,要么被删除了,要么就是添加新的评论,因此不需要清除缓存,将 kHYBRecalculateForStateKey对应的值设置为NO即可。因为我们在上面一步配置外部cell的数据的时候,已经调用过下面计算行高的方法来计算了并且会自动缓存起来,所以这一步的调用其实只是直接获取缓存的高度,因为效率是非常高的。
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
HYBCommentModel*model =[self.testModel.commentModels objectAtIndex:indexPath.row];
return[HYBCommentCell hyb_heightForTableView:self.tableView config:^(UITableViewCell*sourceCell){
HYBCommentCell*cell =(HYBCommentCell*)sourceCell;
[cell configCellWithModel:model];
} cache:^NSDictionary*{
return@{kHYBCacheUniqueKey: model.cid,
kHYBCacheStateKey :@"",
kHYBRecalculateForStateKey: @(NO)};
}];
}
增加、删除评论
选中某条评论的时候,就增加一条评论,然后通过代理反馈到控制器类刷新数据。同样,当删除一条评论的时候,也通过代理反馈到控制器类,刷新数据:
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
- (void)tableView:(UITableView*)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// 添加一条数据
HYBCommentModel*model =[[HYBCommentModelalloc] init];
model.name= @"标哥";
model.reply= @"标哥的技术博客";
model.comment= @"哈哈,我被点击后自动添加了一条数据的,不要在意我~";
model.cid= [NSString stringWithFormat:@"commonModel%ld", self.testModel.commentModels.count+ 1];
[self.testModel.commentModels addObject:model];
if([self.delegate respondsToSelector:@selector(reloadCellHeightForModel:atIndexPath:)]){
self.testModel.shouldUpdateCache= YES;
[self.delegate reloadCellHeightForModel:self.testModel atIndexPath:self.indexPath];
}
}
-(BOOL)tableView:(UITableView*)tableView canEditRowAtIndexPath:(NSIndexPath*)indexPath{
returnYES;
}
-(void)tableView:(UITableView*)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath*)indexPath{
if(editingStyle== UITableViewCellEditingStyleDelete){
[self.testModel.commentModels removeObjectAtIndex:indexPath.row];
if([self.delegate respondsToSelector:@selector(reloadCellHeightForModel:atIndexPath:)]){
self.testModel.shouldUpdateCache= YES;
[self.delegate reloadCellHeightForModel:self.testModel atIndexPath:self.indexPath];
}
}
}
控制器刷新数据
当增加或者删除一条评论的时候,testModel会将shouldUpdateCache设置为YES,以便在reload对应的某一行之后,行高可以重新计算并更新缓存的高度而不是使用之前所缓存起来的高度:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
HYBTestModel*model =[self.datasource objectAtIndex:indexPath.row];
CGFloath =[HYBTestCell hyb_heightForTableView:tableView config:^(UITableViewCell*sourceCell){
HYBTestCell*cell =(HYBTestCell*)sourceCell;
[cell configCellWithModel:model indexPath:indexPath];
} cache:^NSDictionary*{
NSDictionary*cache =@{kHYBCacheUniqueKey: model.uid,
kHYBCacheStateKey :@"",
kHYBRecalculateForStateKey: @(model.shouldUpdateCache)};
model.shouldUpdateCache= NO;
returncache;
}];
returnh;
}
#pragma mark - HYBTestCellDelegate
- (void)reloadCellHeightForModel:(HYBTestModel*)model atIndexPath:(NSIndexPath*)indexPath{
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
2、Cell中嵌入UIWebView
难点分析
- 性能问题:如果每一行cell的高度不一样,采用常规做法是很耗性能,然后高度不好处理。
- 自适应高度问题:如果每次都通过加载UIWebView内容成功后才reload行,从而计算高度,这是会进行死循环的。webview加载—>刷新当前行–>又重新加载webview–>又重复刷新高度–>死循环,当然也可以加载一次webview后拿到的高度跟当前刷新加载的高度比较,若相等则不必无限刷新,不过这么做有一个最大的问题,就是当前正常看着看着,突然又刷新变化了~
如果通过以下中的任何一个方法来加载HTML,那么就是常规的做法了,这种做法要在cell中使用,是比较困难而且比较复杂难以控制的:
- (void)loadRequest:(NSURLRequest*)request;
-(void)loadHTMLString:(NSString*)string baseURL:(NSURL*)baseURL;
- (void)loadData:(NSData*)data MIMEType:(NSString*)MIMEType textEncodingName:(NSString*)textEncodingName baseURL:(NSURL*)baseURL;
加载完了内容才能执行stringByEvaluatingJavaScriptFromString方法。
设计思想
笔者没有使用自动布局来计算,这方面也让大家自行发挥吧。笔者只是通过常规的计算方式来学习一下基本的功能。
本demo的设计思想是通过js的方式来计算高度及替换HTML。
关键点如下:
- 获取HTML的高度:在cell的代理方法heightForRowAtIndexPath,我们要计算出HTML的高度,这样才能确定UIWebView的高度。
- 替换原来的HTML:在cell配置数据显示处,直接替换掉原来的HTML,而不是通过WebView所提供的API来加载HTML,这样就可以同步实现得到显示数据
另外,在计算高度是,如何才能计算HTML的内容的高度呢?那么我们必须借助一个UIWebView来处理了,因此,我们还要使用一个专门用来计算HTML高度的UIWebView。
计算HTML高度的UIWebView
- (UIWebView*)webView{
if(_webView== nil){
_webView= [[UIWebViewalloc] init];
CGSizesize =CGSizeMake(self.view.frame.size.width- 20,0);
_webView.frame= CGRectMake(0,0,size.width,size.height);
}
return_webView;
}
看到设置了大小了吗?我们必须指定webview的width否则下面计算的高度是不正确的。但是高度不要指定,设置为0就可以了(因为一旦指定,内容就会自动按照这个高度来自适应的,高度也就不用计算了)。
计算HTML的高度
首先,我们在ViewController中添加了个属性webView,用于负责计算HTML的高度的。因此,将它作为属性,一直都需要使用。代码如下:
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
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
HYBTestModel*model =[self.datasource objectAtIndex:indexPath.row];
CGFloatw =[UIScreenmainScreen].bounds.size.width;
CGFloath =[model.title sizeWithFont:[UIFont systemFontOfSize:16]
constrainedToSize:CGSizeMake(w,CGFLOAT_MAX)
lineBreakMode:NSLineBreakByWordWrapping].height;
h+= 10+ 20;
if(model.webHeight<= 0){
// 注意不能使用双引号
NSString*js =[NSString stringWithFormat:@"document.body.innerHTML = '%@'",model.html];
[self.webView stringByEvaluatingJavaScriptFromString:js];
NSLog(@"%@",[self.webView stringByEvaluatingJavaScriptFromString:@"document.body.innerHTML"]);
NSString*heightJs =@"document.getElementsByTagName('div')[0].scrollHeight";
CGFloatwebHeight =[[self.webView stringByEvaluatingJavaScriptFromString:heightJs] floatValue];
NSLog(@"%f",webHeight);
self.webView.scrollView.contentSize= CGSizeMake(w- 20,webHeight);
model.webHeight= webHeight+ 40;
}
returnh +model.webHeight+ 40;
}
- 先通过document.body.innerHTML这段JS来替换用于计算HTML高度的UIWebView的HTML
- document.getElementsByTagName(‘div’)[0].scrollHeight来获取可滚动范围的高度,注意,这个参数div不是固定的,要根据自己的HTML的内容来获取,不能直接获取body.scrollHeight,这是不对的。对于笔者的demo中,div是最上层标签,所以就可以通过它获取高度。
- 为什么要将计算的结果加上40?因为直接计算出来的是不够准备的,因为还有margin、padding,自己看看差不多就可以了。
配置Cell
- (void)configCellWithModel:(HYBTestModel*)model{
self.label.text= model.title;
[self.labelsizeToFit];
NSString*js =[NSString stringWithFormat:@"document.body.innerHTML = '%@'",model.html];
[self.webView stringByEvaluatingJavaScriptFromString:js];
self.webView.frame= CGRectMake(10,
self.label.frame.origin.y+ self.label.frame.size.height+ 20,
self.label.frame.size.width,
model.webHeight);
}
对于配置cell显示数据时,直接替换UIWebView里面的HTML就可以了,不需要通过代理回调才通知高度,如此就可以直接设置webview的frame了。
这里可没有再计算HTML的高度了,在计算高度的时候,直接给model添加了属性model.webHeight,用于记录所计算出来的高度,以免重复计算
以上,整理自标哥文章,感谢之~~
0 0