项目中要做块,发起事件着和粉丝互动,相互回复评论.在这块.用到动态布局.接下来就总结下自己写的这块.
一.首先网络请求接口是公司的..不能在这写了,我就自己本地写个 plist 文件,模拟网路请求吧。
NSString*filePath = [[NSBundlemainBundle]pathForResource:@"Data"ofType:@"plist"];
NSArray*dataArr = [[NSArrayalloc]initWithContentsOfFile:filePath];
for(NSDictionary*dictindataArr) {
CommentModel*model = [[CommentModelalloc]init];
[modelsetValuesForKeysWithDictionary:dict];
[self.datasourceaddObject:model];
}
[_tableViewreloadData];
二.网络请求回来数据了,我们应该设计怎么把布局和数据联系在一起.这里难得就是计算文本的高度.应该在什么时候计算.什么时候调用.在这我的思路是在对数据进行处理的时候.顺便把高度计算出来.和数据模型绑定在一起.这样就能和布局的 View 绑定在一起.
@interface ReplayModel : NSObject
/** 评论方式 **/
@property (nonatomic, copy) NSString *CommentType;
/** 评论内容 **/
@property (nonatomic, copy) NSString *Content;
/** 来自评论者的 id **/
@property (nonatomic, strong) NSNumber *FromMemberId;
@property (nonatomic, strong) NSNumber *Id;
/** 被评论者的 id **/
@property (nonatomic, strong) NSNumber *ToMemberId;
/** 被评论者发表的任务 id **/
@property (nonatomic, strong) NSNumber *ToTaskId;
@end
@interface CommentModel : NSObject
/** 评论方式 **/
@property (nonatomic, copy) NSString *CommentType;
/** 内容 **/
@property (nonatomic, copy) NSString *Content;
/** 创建时间 **/
@property (nonatomic, copy) NSString *CreateDateStr;
/** 来自评论者的名称 **/
@property (nonatomic, copy) NSString *FromNickname;
/** 来自评论者的头像 **/
@property (nonatomic, copy) NSString *FromHeadImage;
/** 来自评论者的id **/
@property (nonatomic, strong) NSNumber *FromMemberId;
@property (nonatomic, strong) NSNumber *Id;
/** 回复 **/
@property (nonatomic, strong) NSArray *Replay;
/** 内容高度 **/
@property (nonatomic, assign) CGFloat contentHeight;
@end
这是我的数据模型.在大的数据模型里面嵌套了一个专门关于评论的回复的数据模型.在. m 的才是中点.
- (void)setNilValueForKey:(NSString *)key{
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
- (void)setValue:(id)value forKey:(NSString *)key{
if ([key isEqualToString:@"Content"]) {
//计算高度
CGFloat replyHeight = [[value stringByAppendingString:@"回复: "] boundingRectWithSize:CGSizeMake(SCREENWIDTH - 80, MAXFLOAT) options:NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:16]} context:nil].size.height;
//拼接到数据中
value = [NSString stringWithFormat:@"%f#回复: %@",replyHeight,value];
[super setValue:value forKey:key];
}else{
[super setValue:value forKey:key];
}
}
@end
@implementation CommentModel
- (void)setNilValueForKey:(NSString *)key{
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues{
[super setValuesForKeysWithDictionary:keyedValues];
//计算文本高度
[keyedValues enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
if ([key isEqualToString:@"Content"]) {
self.contentHeight = [obj boundingRectWithSize:CGSizeMake(SCREENWIDTH - 103, MAXFLOAT) options:NSStringDrawingUsesFontLeading | NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:16]} context:nil].size.height;
return ;
}
if ([key isEqualToString:@"Replay"]) {
NSMutableArray *relayArr = [NSMutableArray array];
[obj enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
ReplayModel *replayModel = [[ReplayModel alloc] init];
[replayModel setValuesForKeysWithDictionary:obj];
[relayArr addObject:replayModel];
}];
self.Replay = relayArr;
}
}];
}
@end
这里处理是在
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues
这个方法里面做操作.将作者文本高度计算出来.赋值给事先定义的contentHeight这个属性.好处之后说.在判断在key 是Replay.那就是嵌套的回复评论的 model. 所以这时要对它处理,一一赋值.
在ReplayModel的
- (void)setValue:(nullable id)value forKey:(NSString *)key;
方法里面计算高度.这时候因为是不固定的高度,所以不能为这个 model 设置属性.所以想到直接将计算好的高度拼接到评论下面.中间件个#标示符.这时.数据模型算是处理完了.
三.UITableViewCell 里面的操作
这里主要有几个东西.
/** 存放评论lable 高度的数组 **/
@property (nonatomic, strong) NSMutableArray *replayLabelHeights;
/** 存放评论lable文本的数组 **/
@property (nonatomic, strong) NSMutableArray *replayLabelTexts;
用两个数据去存储所有缓存的高度和文本的内容
接下来就是重新设置头条问题的高度.和动态添加回复评论
//如果是没回复的,返回
if (commentModel.Replay == nil) {
return ;
}
//存放评论lable 高度的数组
_replayLabelHeights = [NSMutableArray array];
_replayLabelTexts = [NSMutableArray array];
//回复
[commentModel.Replay enumerateObjectsUsingBlock:^(ReplayModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
ReplayModel *replayModel = obj;
//分割数据.收集评论lable的高度和文本
[_replayLabelHeights addObject:[replayModel.Content componentsSeparatedByString:@"#"].firstObject];
NSString *textStr = @"";
int i = 0;
for (NSString *text in [replayModel.Content componentsSeparatedByString:@"#"]) {
i ++;
if (i == 1) {
continue;
}
textStr = [textStr stringByAppendingString:text];
}
[_replayLabelTexts addObject:textStr];
}];
//动态添加回复 label
for (int i = 0; i < _replayLabelHeights.count; i ++) {
CGFloat height = [self addHeightWithI:i - 1];
UILabel *replyLabel = [[UILabel alloc] initWithFrame:CGRectMake(76, _contentLabel.bottomY + height, SCREENWIDTH - 80, [_replayLabelHeights[i] floatValue])];
replyLabel.numberOfLines = 0;
replyLabel.layer.borderColor = [UIColor lightGrayColor].CGColor;
replyLabel.layer.borderWidth = 1;
replyLabel.backgroundColor = [UIColor colorWithRed:200 / 255.0 green:205 / 255.0 blue:246 / 255.0 alpha:1];
replyLabel.font = [UIFont systemFontOfSize:16];
replyLabel.text = _replayLabelTexts[i];
[self.contentView addSubview:replyLabel];
}
动态计算设置 Y 时,拥到这样个方法.
- (CGFloat)addHeightWithI:(int)i{
__block CGFloat height = 0;
//第一行直接返回,不取值
if (i < 0) {
return height;
}
[_replayLabelHeights enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
height += [obj floatValue];
height += 8;
//取到对应行后返回值
if (idx == i) {
*stop = YES;
}
}];
return height;
}
来确定动态添加的回复评论的 lable 的位置
四.最后就是UIViewController的协议方法的东西了.这里说以下两个方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
if (indexPath.row == 0) {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:indentifierNormal forIndexPath:indexPath];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = @"评论";
return cell;
}
CommmentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:indentifier forIndexPath:indexPath];
cell.assignment(self.datasource[indexPath.row - 1]);
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
if (indexPath.row == 0) {
return 44;
}
CommentModel *commentModel = self.datasource[indexPath.row - 1];
//51为第一个内容剧 cell 上的距离
CGFloat cellHeight = 51 + (commentModel.Replay.count * 8) + commentModel.contentHeight;
//如果没有回复,直接返回
if (commentModel.Replay == nil) {
//8为据底部的边距
return cellHeight + 8;
}
//计算回复的评论高度
for (ReplayModel *replayModel in commentModel.Replay) {
NSArray *replayArr = [replayModel.Content componentsSeparatedByString:@"#"];
cellHeight += [replayArr.firstObject floatValue];
}
return cellHeight;
}
这里直接从计算好的数据源中取高度..因此这里不会造成性能的影响.
现在差不多就完成了.这里是效果图,虽然有点丑.
总结:
1.之所以把计算耗时的操作放到数据处理的里面去.一是为了与数据模型绑定,因而和 View 来绑定.达到高聚合.二是这是一个费时的操作.应为网络请求会等待时间.这个是无法改变的.所以,把他放这和网络请求中增加那么一小点时间还是可以的.三是放在模型转换的方法中,只会计算一次.达到高效率.起到缓存的效果.有人会问,为啥不放到子线程.这个数据回来就要刷新界面.意味这就得布局界面,这时不能保证已经算好.所以不能放到子线程.
2.也调研了一些,说可以吧计算耗时的东西方法 RUNloop 的空闲时候.这个应该不错,但是水平有限,以后补上.
3.其实这里犯了个错误,就是尽量不要动态在 UITableViewCell 上添加子控件.但是,这地方没办法.以后优化补上.
最后.这里说下作者测得的性能:
在这可以看出.在计算方法里面是好了点时间.但是在
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
这个方法里面耗时.这个方法是调用时机是很频繁的.在 tableView 初始化的时候,要调用多少个cell个这个方法.来确定 contenetSize ,在 每次调用cellForRow方法又得调用一次,所以说.这样写的话,能稍微好点.
这里留个 demo 的地址:.