在日常的开发中,有时会遇到内容块比较多,且又可变的界面:
多个可变cell复杂界面这个界面中有些内容块是固定出现的,比如最上面的商品详情图片、商品名称、价格等。而有些内容块则是不一定出现的,比如促销(显然不是每个商品都有促销)、已选规格(有的商品没有规格)、店铺信息(有的商品属于自营,就没有店铺)等。还有些内容要根据情况进行变化,比如评论,这里最多列出4条评论,如果没有评论,则显示“暂无评论”且不显示“查看所有评论”按钮。
对于这样的界面,相信很多人第一感觉会用TableView来做,因为中间要列出评论内容,这个用TableView的cell来填充比较合适。但如何处理评论内容之外的其他内容呢?我之前的做法是,评论内容之上的用HeaderView做,下面的用FooterView做,虽然最终实现了功能,但做起来十分麻烦。布局我是用Auto Layout来做的,由于Auto Layout本身的特点,这种做法在控制内容块View的显示与否,需要比较多的操作:
- View的高度约束设置为0
- 最好还要把子View全部移除,否则,子View里的约束可能会因为View高度约束设置为0而出现约束冲突
- 如果View本身有距离前面View的间距约束,也需要将间距约束设置为0
- View的hidden设置为yes,以减少视图绘制
此外,还有一个麻烦的问题。界面刚进来的时候,是需要请求网络数据,这时界面就要显示成一个初始状态,而显然初始状态有些内容块是不应该显示的,比如促销,只有完成了数据请求,才能知道是否有促销,有的话才显示促销内容;比如评论,初始时应该显示成“暂无评论”,数据请求完成后,才显示相应的内容。这样,我们需要处理初始进入和数据请求完成两种状态下各个内容块的显示,十分复杂繁琐。
总结来说,用TableView的 HeaderView + 评论内容cell + FooterView + Auto Layout 的方式会带来如下问题:
- 约束在View与View之间是有依赖关系的,对View的显示与否,需要比较多的操作
- 需要处理初始进入和数据请求完成两种状态的界面展示,使代码更加复杂繁琐
- 需要额外计算相应内容的高度,以更新HeaderView、FooterView的高度
可见,这种方式并不是理想的解决方案。可能有人会说,那不要用Auto Layout,直接操作frame来布局就好,这样或许能减少一些麻烦,但总体上并没有减少复杂度。也有人说,直接用ScrollView来做,这样的话,所有的内容包括评论内容的cell,都得自己手动拼接,可以想象这种做法也是比较麻烦的。所以,我们得另辟蹊径,使用其他方法来达到目的。下面就为大家介绍一种比较简便的做法,这种做法也是一个前同事分享给我的,我就借花献佛,分享给大家。
我们还是用TableView来做这个界面,和之前不同的是,我们把每一个可变内容块做成一个独立的cell,cell的粒度可以自行控制,比如可以用一个cell囊括商品图片、标题、副标题、价格,也可以拆得更细,图片、标题、副标题、价格都各自对应一个cell。这里我们选择后者,因为图片内容块,我们需要按屏幕宽度等比例拉伸;标题、副标题的文字内容可能是一行,也可能是两行,高度可变,用单独的cell来控制会更简单明了,也更加灵活。
下面先定义好各种类型的cell:
//基础cell,这里为了演示简便,定义这个cell,其他cell继承自这个cell
@interface MultipleVariantBasicTableViewCell : UITableViewCell
@property (nonatomic, weak) UILabel *titleTextLabel;
@end
//滚动图片
@interface CycleImagesTableViewCell : MultipleVariantBasicTableViewCell
@end
//正标题
@interface MainTitleTableViewCell : MultipleVariantBasicTableViewCell
@end
//副标题
@interface SubTitleTableViewCell : MultipleVariantBasicTableViewCell
@end
//价格
@interface PriceTableViewCell : MultipleVariantBasicTableViewCell
@end
// ...其他内容块的cell声明
// 各种内容块cell的实现,这里为了演示简便,cell中就只放了一个Label
@implementation MultipleVariantBasicTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 44)];
label.numberOfLines = 0;
[self.contentView addSubview:label];
self.titleTextLabel = label;
}
return self;
}
@end
@implementation CycleImagesTableViewCell
@end
@implementation MainTitleTableViewCell
@end
// ...其他内容块的cell实现
// 评论内容cell使用Auto Layout,配合iOS 8 TableView的自动算高,实现内容自适应
@implementation CommentContentTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.titleTextLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.titleTextLabel.preferredMaxLayoutWidth = [UIScreen mainScreen].bounds.size.width - 8;
NSLayoutConstraint *leftConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeLeading multiplier:1.0f constant:4.0f];
NSLayoutConstraint *rightConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTrailing multiplier:1.0f constant:-4.0f];
NSLayoutConstraint *topConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTop multiplier:1.0f constant:4.0f];
NSLayoutConstraint *bottomConstraint = [NSLayoutConstraint constraintWithItem:self.titleTextLabel attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeBottom multiplier:1.0f constant:-4.0f];
[self.contentView addConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]];
}
return self;
}
@end
接下来就是重点,就是如何来控制显示哪些cell及cell显示的数量。这一步如果处理不好,也会使开发变得复杂。如下面的方式:
// 加载完数据
self.cellCount = 0;
if (存在促销) {
self.cellCount++;
}
if (存在规格) {
self.cellCount++;
}
......
如果以这种方式来记录cell的数量,那么后续cell的展示、点击判断等都会很麻烦。这里我们采用的方式是,使用单独的类(作为一种数据结构)来保存所要展示的cell信息。
// SKRow.h
@interface SKRow : NSObject
@property (nonatomic, copy) NSString *cellIdentifier;
@property (nonatomic, strong) id data;
@property (nonatomic, assign) float rowHeight;
- (instancetype)initWithCellIdentifier:(NSString *)cellIdentifier
data:(id)data
rowHeight:(float)rowHeight;
@end
// SKRow.m
#import "SKRow.h"
@implementation SKRow
- (instancetype)initWithCellIdentifier:(NSString *)cellIdentifier data:(id)data rowHeight:(float)rowHeight {
if (self = [super init]) {
self.cellIdentifier = cellIdentifier;
self.data = data;
self.rowHeight = rowHeight;
}
return self;
}
@end
SKRow用来存储每个cell所需的信息,包括重用标识、数据项、高度。接下来,我们就开始拼接cell信息。
@interface ViewController () <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, strong) NSMutableArray<NSArray<SKRow *> *> *tableSections;
@end
self.tableSections = [NSMutableArray array];
/* 初始加载数据
* 初始化时,只显示滚动图片、价格、评论头、无评论
*/
// 滚动图片(宽高保持比例)
SKRow *cycleImagesRow = [[SKRow alloc] initWithCellIdentifier:@"CycleImagesCellIdentifier" data:@[@"滚动图片地址"] rowHeight:120*[UIScreen mainScreen].bounds.size.width / 320.f];
// 价格
SKRow *priceRow = [[SKRow alloc] initWithCellIdentifier:@"PriceCellIdentifier" data:@"0" rowHeight:44];
[self.tableSections addObject:@[cycleImagesRow, priceRow]];
// 评论头
SKRow *commentSummaryRow = [[SKRow alloc] initWithCellIdentifier:@"CommentSummaryCellIdentifier" data:@{@"title":@"商品评价", @"count":@"0"} rowHeight:44];
// 无评论
SKRow *noCommentRow = [[SKRow alloc] initWithCellIdentifier:@"NoCommentCellIdentifier" data:@"暂无评论" rowHeight:44];
[self.tableSections addObject:@[commentSummaryRow, noCommentRow]];
以上是初始状态时要显示的cell,我们在ViewController中声明一个数组,用来存储TableView各个section要显示的cell信息。这里我们将cell分成不同的section,实际中,要不要分,分成几个section都可以自行决定。初始状态我们有两个section,第一个section用于显示基本信息,第二个section用于显示评论信息,这样就完成了cell信息的拼接,接下来就是显示:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// 这里可以通过判断cellIdentifier来区分处理各种不同的cell,cell所需的数据从row.data上获取
SKRow *row = self.tableSections[indexPath.section][indexPath.row];
if ([row.cellIdentifier isEqualToString:@"CycleImagesCellIdentifier"]) {
CycleImagesTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
NSArray<NSString *> *urlStringArray = row.data;
cell.titleTextLabel.text = [urlStringArray componentsJoinedByString:@"\n"];
return cell;
} else if ([row.cellIdentifier isEqualToString:@"MainTitleCellIdentifier"]) {
MainTitleTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
cell.titleTextLabel.text = row.data;
return cell;
} else if ([row.cellIdentifier isEqualToString:@"PriceCellIdentifier"]) {
PriceTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
cell.titleTextLabel.text = [NSString stringWithFormat:@"¥%@", row.data];
return cell;
} else if ([row.cellIdentifier isEqualToString:@"SalePromotionCellIdentifier"]) {
SalePromotionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
NSArray<NSString *> *salePromotionStringArray = row.data;
cell.titleTextLabel.text = [salePromotionStringArray componentsJoinedByString:@"\n"];
return cell;
} else if ([row.cellIdentifier isEqualToString:@"SpecificationCellIdentifier"]) {
SpecificationTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
cell.titleTextLabel.text = [NSString stringWithFormat:@"已选:%@", row.data];
return cell;
} else if ([row.cellIdentifier isEqualToString:@"CommentSummaryCellIdentifier"]) {
CommentSummaryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
NSDictionary *commentSummary = row.data;
cell.titleTextLabel.text = [NSString stringWithFormat:@"%@(%@)", commentSummary[@"title"], commentSummary[@"count"]];
return cell;
} else if ([row.cellIdentifier isEqualToString:@"CommentContentCellIdentifier"]) {
CommentContentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
cell.titleTextLabel.text = row.data;
return cell;
} else if ([row.cellIdentifier isEqualToString:@"AllCommentCellIdentifier"]) {
AllCommentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
cell.titleTextLabel.text = row.data;
return cell;
} else if ([row.cellIdentifier isEqualToString:@"NoCommentCellIdentifier"]) {
NoCommentTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
cell.titleTextLabel.text = row.data;
return cell;
}
return nil;
}
上面的代码进行了删减,没有处理所有类型。虽然稍嫌冗长,但是逻辑非常简单,就是获取cell信息,根据重用标识来区分不同类型的内容块,将数据处理后放到cell中展示。
例如,对于商品图片,因为是滚动图片,滚动图片可以有多张,前面我们传入的数据就是数组data:@[@"滚动图片地址"]
。后面获取到数据后,cell.titleTextLabel.text = [urlStringArray componentsJoinedByString:@"\n"];
,出于演示,商品图片cell我们只放了一个Label,所以只是简单的将地址信息分行显示出来。在实际的开发中,可以放入一个图片滚动显示控件,并将图片地址的数组数据传给控件展示。
其他类型的cell处理也是大同小异,出于演示的原因,都只是简单的数据处理展示。当然,别忘了,设置一下TableView相关的dataSource和delegate:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
SKRow *row = self.tableSections[indexPath.section][indexPath.row];
return row.rowHeight;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.tableSections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.tableSections[section].count;
}
这样我们就完成了初始状态时界面的展示:
初始状态时界面显示完成了cell的显示处理,接下来我们来模拟一下网络请求数据后,界面如何显示所需的cell:
self.tableSections = [NSMutableArray array];
NSMutableArray<SKRow *> *section1 = [NSMutableArray array];
// 滚动图片(宽高保持比例)
SKRow *cycleImagesRow = [[SKRow alloc] initWithCellIdentifier:@"CycleImagesCellIdentifier" data:@[@"滚动图片地址1", @"滚动图片地址2", @"滚动图片地址3"] rowHeight:120*[UIScreen mainScreen].bounds.size.width / 320.f];
// 主标题
SKRow *mainTitleRow = [[SKRow alloc] initWithCellIdentifier:@"MainTitleCellIdentifier" data:@"商品名称" rowHeight:44];
// 副标题
SKRow *subTitleRow = [[SKRow alloc] initWithCellIdentifier:@"SubTitleCellIdentifier" data:@"节日促销,快来买啊" rowHeight:44];
// 价格
SKRow *priceRow = [[SKRow alloc] initWithCellIdentifier:@"PriceCellIdentifier" data:@(arc4random()) rowHeight:44];
[section1 addObjectsFromArray:@[cycleImagesRow, mainTitleRow, subTitleRow, priceRow]];
// 促销(随机出现)
if (arc4random() % 2 == 0) {
SKRow *salePromotionRow = [[SKRow alloc] initWithCellIdentifier:@"SalePromotionCellIdentifier" data:@[@"促销信息1", @"促销信息2", @"促销信息3"] rowHeight:44];
[section1 addObject:salePromotionRow];
}
[self.tableSections addObject:section1];
NSMutableArray<SKRow *> *section2 = [NSMutableArray array];
// 规格(随机出现)
if (arc4random() % 2 == 0) {
SKRow *specificationRow = [[SKRow alloc] initWithCellIdentifier:@"SpecificationCellIdentifier" data:@"银色,13.3英寸" rowHeight:44];
[section2 addObject:specificationRow];
}
if (section2.count > 0) {
[self.tableSections addObject:section2];
}
NSMutableArray<SKRow *> *section3 = [NSMutableArray array];
NSArray<NSString *> *commentArray = [NSMutableArray array];
// 评论内容数据(随机出现)
if (arc4random() % 2 == 0) {
commentArray = @[@"评论内容1", @"评论内容2", @"2016年6月,苹果系统iOS 10正式亮相,苹果为iOS 10带来了十大项更新。2016年6月13日,苹果开发者大会WWDC在旧金山召开,会议宣布iOS 10的测试版在2016年夏天推出,正式版将在秋季发布。2016年9月7日,苹果发布iOS 10。iOS10正式版于9月13日(北京时间9月14日凌晨一点)全面推送。", @"评论内容4"];
}
// 评论头
SKRow *commentSummaryRow = [[SKRow alloc] initWithCellIdentifier:@"CommentSummaryCellIdentifier" data:@{@"title":@"商品评价", @"count":@(commentArray.count)} rowHeight:44];
[section3 addObject:commentSummaryRow];
if (commentArray.count > 0) {
for (NSString *commentString in commentArray) {
// 评论内容需要自适应高度,高度值指定为UITableViewAutomaticDimension
SKRow *commentContentRow = [[SKRow alloc] initWithCellIdentifier:@"CommentContentCellIdentifier" data:commentString rowHeight:UITableViewAutomaticDimension];
[section3 addObject:commentContentRow];
}
// 查看所有评论
SKRow *allCommentRow = [[SKRow alloc] initWithCellIdentifier:@"AllCommentCellIdentifier" data:@"查看所有评论" rowHeight:44];
[section3 addObject:allCommentRow];
} else {
// 无评论
SKRow *noCommentRow = [[SKRow alloc] initWithCellIdentifier:@"NoCommentCellIdentifier" data:@"暂无评论" rowHeight:44];
[section3 addObject:noCommentRow];
}
[self.tableSections addObject:section3];
[self.tableView reloadData];
上面的代码同样比较冗长,但逻辑也同样十分简单。按显示顺序拼凑cell数据,有些不一定显示的内容块,如促销,则随机判断,如果显示,将数据加入到section数组中[section1 addObject:salePromotionRow];
。其他类型的cell也是类似的,不再赘述。要注意的是,评论内容的文本可能有多行,我们将它的cell高设置为UITableViewAutomaticDimension:
[[SKRow alloc] initWithCellIdentifier:@"CommentContentCellIdentifier" data:commentString rowHeight:UITableViewAutomaticDimension];
由于评论内容cell我们使用了Auto Layout,这样就可以利用iOS 8 TableView的新特性,自动计算cell的高度。拼接完数据后,只要调用[self.tableView reloadData];
让TableView重新加载即可。
好了,这样就大功告成:
最终效果使用上述方式制作这种内容块可变的界面虽然写起来较为啰嗦,但有如下优点:
- 逻辑清晰简单,易于理解。视图间不存在像先前HeaderView + Auto Layout + FooterView那种麻烦的约束处理,内容块的显示与否处理非常简便。
- 性能比较好。有些cell可以复用,减少开销。并且只加载需要显示的View,如果是之前的做法,或者用scrollView来做,虽然最终也是只显示需要的View,但不需要显示的View还是要加载进来,有性能损耗。
- 易于静态调整。如果产品经理要求调换内容块的显示顺序,只要移动下拼凑cell数据的代码顺序即可。如果是去除某个内容块,代码上的调整也不复杂。
- 易于动态调整内容块的显示顺序。所谓的动态调整,是指界面要根据接口返回的数据,来决定哪些内容块显示在前面,哪些显示的后面。比如接口返回type=0时,价格项显示在商品名称之上,而type=1时,价格项显示在商品子标题之下。
- 易于处理相似但又不同的界面。比如商品有好几种不同的类型,有特惠专区,有免费专区的。免费专区的商品详情在价格内容块上要显示不一样的内容。这时,就可以多做一种类型的cell,根据接口返回type进行判断,如果是免费专区则选取免费专区的cell来显示。用之前HeaderView + Auto Layout的做法,就要费神地去调整约束,事倍功半。
- 易于扩展增加新的内容块。要增加新的内容块,只需创建新的cell,在数据拼接时,增加拼接新cell类型的数据代码,同样在显示的地方增加显示新cell类型的代码即可,几乎不需要修改原有的逻辑。
最后,附上Demo工程代码。注意,这个工程是用XCode 8创建的,低版本的XCode可能运行会有问题(XCode 8的storyboard默认好像不兼容老版本),示例是基于iOS 8,如果要兼容老版本,请自行修改(主要是涉及cell自动算高的部分)。
后记
写这篇文章之初,只是作为一个note,想着有哪个做iOS的朋友遇到类似的问题,可以给他做个参考。没想到,竟引来不少关注,还被推上公众号,收到不少评论,自己也因此打了一些“口水仗”。
从中,我也意识到我少强调了一件“显而易见”的事件。我想说,我的方法并不适用所有情况,也不是要解决所有的问题。那些持批评观点的,大多是面临的问题需求不同所致。就好比我的方法是一把切水果的刀,而你要拿它去剁骨头,那当然是不行的,它本来就不是用来剁骨头的。
当然,很庆幸的是,从这些讨论批评中,我也发现这种方法的一些不足,也有不少有益的收获。
不足之处在于:
-
不大适用于交互比较多的界面。如:点击某个按钮显示/隐藏某个数据项、填写表单项等
交互比较多的界面
像这个界面,选了优惠券后,要更新显示优惠信息,更新对应的应付款,用这种方法就不方便了
- 多个接口获取数据并依次展示内容项。比如基本信息一个接口、促销信息一个接口、评论信息一个接口,请求到基本信息数据就要展示基本信息,请求到促销数据就要展示促销信息,以此类推,那么数据拼接会比较麻烦
收获在于,因此认识了@sun6boys,多了一个朋友,可谓是不打不相识。虽然他在文章的评论中并没有详细展示他的方法,但在私下的讨论中,我已经窥探到了他的方法全貌。他对我方法不足的指责是有道理的,他的方法在数据的处理上比我规整,我的方法显得原始粗暴。对于有交互的界面及从多个接口获取数据依次展示的处理上,也要更加容易,整体的思路实现也非常简洁,可以看作是我这种方法的升级改进版。更难能可贵的是,他为此专门写了一个demo放到了github上:https://github.com/sun6boys/CRVisibleCellsDemo,大家可以学习参考。
同样,也有其他的解决方法,比如完全用scrollview实现的:《复杂界面开发之所思》。我并不赞同用scrollview来做这种界面,原因在那篇文章也多有评论,但不管如何,多参考下其他的方法也是有益处的,至于如何取舍,就看各位的选择了。
其实不论用什么方法,都是一种权衡,需要根据自身的情境去考虑。比如你所面临的需求,是纯展示型的界面,还是交互比较多的界面。比如团队的开发习惯,我的团队比较不习惯用scrollview,自然解决方法就会向tableview靠。比如团队技能、学习成本,有的方法对约束的使用要求较高,这就是一种技能要求、一种门槛,会对团队开发和新人融入产生影响。有人说我的方法偏傻瓜式,这是对的,因为这也是我所追求的。傻瓜式就意味着容易上手,团队成员可以很容易使用这个方法,不管是接手别人的代码,还是有新人进入团队,做这一块的东西,都不会多高的门槛。并且这个方法也已经足够解决目前的问题,我觉得这样就够了。
所以,我觉得用哪种方法都不奇怪,甚至综合各种因素后,使用H5去做也可以啊。当然,那样的话,也就没iOS多少事了。
网友评论
第二步的方法,如果reload,键盘就会收起,目前理解,好像只有单独贴个view在cell下面最可行,最近测试了下,发现微信也是用这种思路,他的录入框在切换输入法的时候会剥离cell,应该uitextview没有在cell里
在tableView的cell弄这样的textView确实是件苦差事。因为没有做过类似你这种东西,我不好解答,只能说给你一些可能的建议:
1. 你可以尝试用beginUpdates、endUpdates,而不要用reload,并且可以尝试关闭隐式动画
2. 你可以在reload之前记下tableView的contentOffset,然后调用reload,再调用layoutIfNeeded,再重新设置tableView的contentOffset为先前的值(设置时用setContentOffset:animated,且animated置为false)
你说这个方法偏傻瓜式,这是对的,这也是我所追求的。任何方法都是一种权衡,傻瓜式就意味着容易上手,团队成员可以很容易使用这个方法,不管是接手别人的代码,还是有新人进入团队,做这一块的东西,都不会多高的门槛。并且这个方法也已经足够解决目前的问题,我觉得这样就够了。
我也看过别的解决方法,也看过别的批评。我想说,我的方法没有要尝试解决所有问题。比如有的人的界面不仅多变,而且还有较多的交互(如点击按钮会展开/隐藏某些项、有好多表单项需要填写等),这时用我这种方法就不那么好,我这种方法是适用于cell多变但纯展示的界面。如果因此说,这个方法扩展性不好这类的,我也只能表示无奈。这一切都看你考虑的因素以及怎么取舍,正如我所说,这是一种权衡。如果要设计一种更通用的机制,从项目的角度出发,那需要花费更多的时间精力,而项目的时间往往是有限的。从团队的角度出发,这也可能导致学习曲线升高,对团队开发、新人融入、代码维护造成一定的门槛,学习成本也是要考虑的。如果你是光杆司令,或是时间不紧,或是要应对更复杂的界面,那可以另谋他法,这都取决于你的情境。
请问,什么样才算深入理解tableview的机制?
* 大量tableview的使用(包括静态表)
* 研读过[更轻量的 View Controllers](https://www.objccn.io/issue-1-1/)
* 研读过[整洁的 Table View 代码](https://www.objccn.io/issue-1-2/)
* 研读过[Pro iOS Table Views: for iPhone, iPad, and iPod touch](https://www.amazon.com/Pro-iOS-Table-Views-iPhone/dp/1430233486/ref=sr_1_2?ie=UTF8&qid=1486305485&sr=8-2&keywords=Pro+iOS+Table+Views)
* 研读过[通过实现TableView来理解IOS编程](https://www.gitbook.com/book/yishuiliunian/implementate-tableview-to-understand-ios/details)
* 看过tableview的[“模拟源码”](https://github.com/BigZaphod/Chameleon/blob/master/UIKit/Classes/UITableView.m)
* 封装过tableview
这样算不算深入理解?也许按你的逻辑,在处理这种可变cell问题时,没有用你那种方式就不是深入理解,这真是可笑至极。每种解决方法都有各自的优劣,这是一种综合权衡。例如,哪种方式更易于处理cell间的空白间隔,哪种方法更易于理解使用,更适用当前项目需求……。所要考量的,不仅仅是技术的问题,不一定是别人没有深入理解,而只是做出了权衡。
动不动就是“教育”,一副高高在上,别人得了你的“指教”,就是荣幸之至,应该感恩戴德、涕泗横流。在现实中,你所遇到的人你都知根知底,你爱显示你的优越感去“教育”他们,那是你的事情。但在网络上,你对别人又了解多少?我虽然不是什么高手,但也不是刚出道的菜鸟,不需要你这种毫无营养的“指教”。我在文章中也说过,这篇文章是“借花献佛”,是从一个前同事那边学到这个方法,前同事又是从他的leader那边学的。这个方法并不是我想出来,我只是写出而已,当然如果是我想的,我会很开心,因为我觉得它解决问题的方式很简洁高效。我那同事的leader也是有十几年的开发经验,这个方法也是他在某个大公司期间所使用的,必定也是经过成熟的考量和检验,你觉得这样是你能“教育”得了的吗?
最后,最为关键的,我并不认为你的方法会更好更简单。挺多某些方面好一些,但肯定某些方面会有缺点(比如cell间空白间隔的处理就会比较麻烦)。综合来说,我觉得最多也就是半斤八两,我希望你能把你的方法完整的叙述出来,然后去看看,cell间空白间距处理,cell的增加、减少、顺序调整、显示方式调整(如没有评论时,不显示评论块)等这些问题是否易于处理,毕竟需求可能改变,解决方法要有灵活性和扩展性。
法国学者费马在阅读丢番图(Diophatus)《算术》拉丁文译本时,曾在第11卷第8命题旁写道:“将一个立方数分成两个立方数之和,或一个四次幂分成两个四次幂之和,或者一般地将一个高于二次的幂分成两个同次幂之和,这是不可能的。关于此,我确信已发现了一种美妙的证法 ,可惜这里空白的地方太小,写不下。”
费马没有写下证明,而关于他猜想的证明却难住后来的数学家长达350多年之久。直到1995年,才由安德鲁·怀尔斯递交论文解决,论文本身长达130页,即使费马被誉为“业余数学家之王”,也不禁让人怀疑他当初所声称的证法,是否真正那么简洁美妙。希望你也不会像他一样,留下一句“我有一个巧妙的解决方法,但手机打字不方便写不下……”,然后就杳无音讯。
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 14;
}
这里假定最大的行数为14(各种cell+最多4条评论内容cell,懒得去算真实的数目,反正就一个意思),这个数字是写死,还是通过其他方式(如对一个Array进行count)得出,不去讨论,因为我不知道你的具体实现。
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section == 4 && 不存在促销信息) {
return 0;
}
if (section == 7 && 不存在第一条评论) {
return 0;
}
if (section == 8 && 不存在第二条评论) {
return 0;
}
.... // 其他section判断
return 1;
}
你的意思应该是类似上面这样的实现。
我不知道你如何判断当前section是什么类型,上面的示例中是直接拿数字来比较,也许你有其他方式(比如枚举),否则如果需求要求调换cell的顺序,那会导致很麻烦的修改。
另外,我也不知道你是在何处判断相应的业务(例如存不存在促销信息),也许是在其他地方拼好了某个数组数据,然后这边直接`return sectionRowNums[section];`。但总的来说,我相信思路是像上面这样子的。
你这种实现方式会有一个细节问题——cell之间的空白间隔。
如已选规格cell和上面的商品信息还有下面的评论信息都是有间隔的,但已选规格cell不是一定会有,这样,如果你用heightForHeaderInSection或heightForFooterInSection处理,需要额外判断。当然,你也可以用其他方法,如加空白的cell当作间隔去解决,但总归是要麻烦一些。更进一步,如果需求要求没有评论时,就完全不显示评论块(而不是显示成现在这种“暂无评论”),那么这种间隔的判断处理就会更麻烦。
而如果是我的方法,可以直接
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (section == 0) {
return CGFLOAT_MIN;
}
return 10;
}
不用修改现有代码,就可以处理好这种问题。
这种细节也许无关紧要,但魔鬼往往隐藏于细节之中,它会对你的解决方案产生影响。
> 这个代理方法处理好以后后面返回高度和返回cell的方法里面不需要做业务上的判断
我的返回高度的方法不存在任何的业务的判断
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
SKRow *row = self.tableSections[indexPath.section][indexPath.row];
return row.rowHeight;
}
只是直接拿算好的高度返回
cell上所谓的业务判断也就是
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([row.cellIdentifier isEqualToString:@"CycleImagesCellIdentifier"]) {
} else if ([row.cellIdentifier isEqualToString:@"MainTitleCellIdentifier"]) {
}
...
}
判断cell的类型,然后取数据赋给cell展示。我不明白,你所谓在返回cell的方法里面不需要做业务上的判断是如何做的。如果是事先拼好cell放到某个Array中,这里直接拿出来展示,那我不知道你是如何解决cell复用的问题。