在有社交分享平台属性的 App 中,我们经常看见类似有 TableView 中多图展示。不管是发布的表单界面中,还是社交动态的时间线的界面中,都需要根据图片数量动态变化界面。最近刚好写了一个这样的界面,花了点时间写了个 Demo 总结一下,希望可以帮助有需要的人。
实现 Demo 效果如下图。
实现原理分析
初步思路是在 Tableview 的 cell 中内嵌一个 CollecitonView,CollectionView 高度动态变化并是 CollectionView 所在的 Tableview 的 cell 的高度动态变化。总结起来我们需要这几件事:
1.实现一个 Tableview 并自定义一种 TableViewCell 并实现高度自适应
2.在 TableViewCell 中实现 CollectionView 并实现高度动态变化
3.自定义 CollectionViewCell 中实现按钮点击事件(如删除,跳转),数据展示操作
1.实现 Tbleview 和自定义 Tableviewcell
Tableview 就很简单,storyboard 或者代码写一下都可以。实现数据源协议啥的,很普通。要实现 TableViewCell 的高度自适应,一般来说有两种方式,一种是用 iOS 7 后支持的 cel l的 estimatedRowHeight 和 iOS 8 后支持的 self-sizing cells(两者差不多,iOS 8 更完善一些),另一种是用孙源大神的第三方开源库,可以看这篇 文章,两者共同之处都是需要设置 cell 里 contentview 的元素对 cell 的 contenview的四个边的布局约束,换言之要让 cell 里的元素把 cell 四边“撑”起来。Demo里使用了原生的 self-sizing cells 来高度自适应,需要下面两句代码。
self.tableview.estimatedRowHeight = 100.f;//数字为大致估算高度,比如有些 50 有些 100 可以估算 75 左右
self.tableview.rowHeight = UITableViewAutomaticDimension;
如果 Tableview 是用代码创建的,那么 rowheight 属性的默认参数就是 UITableViewAutomaticDimension
,不需要第二行代码。而在 storyboard 或 xib 里拖的默认 rowheight 是拖的 storyboard 属性菜单里的,需要更改为 UITableViewAutomaticDimension
。
接着自定义一个 CDZTableViewCell。并用xib拖上一个collectionview并设置对contenview的autolayout约束。用Masonry之类的第三方布局库或原生进行代码约束也可以。然后在tableview里用registerNib
方法注册一下自定义的cell。
因为 TableView 执行 reloadData 方法时,会把所有 Cell 从 VisableCells 池中移除,并从同一复用标识的复用池中取出 Cell 加入视图中,这个时候 Cell 的高度已经确定好,但 indexPath 的 row 顺序有可能不是原来的,所以不能复用。也就是虽然每一个 Cell 功能一致,但是由于高度和里面图片数量不一样,并不能互相复用。在 Storyboard 中可以用静态 cell 分别放入 CollectionView 实现。这里为了不重复放 CollectionView 用了代码书写 TableView。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *identifer = [NSString stringWithFormat:@"CDZTableViewCell%ld",indexPath.row];//唯一复用标识,相当于不复用
CDZTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifer];
if (!cell) {
cell = [CDZTableViewCell.alloc initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifer];
}
cell.delegate = self;
return cell;
}
当然要实现动态高度变化,我们还需要让 Cell 在合适的时候通知 TableView 应该更新数据和布局了,即调用 TableView 自身的 reloadData
方法。这里我用 Delegate 实现,通知的话也可以,但是通知的要明确接受者和发送者的对应关系,不然有些情况会接受对象不明确(比如实现了两个类似的 Tableview 在视图里)。在 Cell 的头文件里创建 Delegate。
@protocol CDZTableViewCellDelegate<NSObject>
- (void)didChangeCell:(UITableViewCell *)cell;
@end
@interface CDZTableViewCell : UITableViewCell
@property (nonatomic,assign) id<CDZTableViewCellDelegate> delegate;
@end
然后让 Tableview 遵守 CDZTableViewCellDelegate 并在 Tableview 的 DataSource 或 Delegate 方法里设置 Cell 的 Delegate,比如
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
//......
cell.delegate = self;
//......
}
也可以在 Delegate 里的 -(void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath
里设置(感觉这个方法更好些,Datasource 应该处理数据相关的,而且有时候会复用抽离出来)。
2.实现 Collectionview 和自定义 Collectionviewcell
Collectionview 在约束上,除了实现 CollecitionView 对 TableviewCell 的上下左右约束外,还要实现其高度约束,因为本质 Collectionview 是 Scrollview 的子类,实现 Scollview 的 Autolayout 其实就是需要实现对其撑起来 ContentView 的约束,而动态实现 ContentView 的高度,则需要我们更新 ContenView 的高度布局约束的值。 CollectionView 也是类似的,我们先设定一个 CollectionView 的高度约束,再在数据更新时更新高度约束。
这里再提一句,CollectionView 本身和 TableView 类似,系统本身也有 self-sizing cells 的API,如下:
self.collectionViewFlowLayout.estimatedItemSize = CGSizeMake(125, 100);
self.collectionViewFlowLayout.itemSize = UICollectionViewFlowLayoutAutomaticSize;
同样也要实现 CollectionViewCell 里元素对 Cell 四边的约束,“撑”起来自动计算高度,但是不知道为何用了之后在一个 Cell 的时候会莫名其妙居中,且 Cell 的 indexPath 有些错乱,根据 Cell 的 indexPath 找出来的 Cell 是错误的,不知道是啥问题,若有人知道可以告诉一声囧。
这里就不用 CollectionView 的大小自适应了,常规的用设置 itemsize 的大小就好了。在 TableViewCell 的 awakeFromNib
方法中,我们让 CollectionView 的 reloadData
更新一下高度布局约束为 CollectionView 的真实高度并调用刚才自定义的 Delegate 去让 TableView reloadData
从而让 Tableview 的 Cell 高度自适应。
- (void)reloadCell{
[self.collectionView reloadData];
[self.collectionView mas_updateConstraints:^(MASConstraintMaker *make) {
make.height.equalTo(@(self.collectionView.collectionViewLayout.collectionViewContentSize.height));
}];
[self.delegate didChangeCell:self];
}
CollectionView 的真实大小其实是他的 Layout 对象的 CollectionViewContentSize 的值。所以我们需要在每次数据发生改变时,更新一下高度约束的值。
还有就是实现 CollectionView 的 Delegate,点击后执行操作,在赋值在 Cell 对应的 Model 里等操作(比如调用相册等)。注意当点击最后一个 CollectionViewCell 时,要在它的后面插入一个数据,也就是说,最后一个总是会保持在最后。
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
//载入数据,如图片等
CDZCollectionViewItem *item = [CDZCollectionViewItem new];
item.image = [UIImage imageNamed:@"example"];
if ((indexPath.row == self.itemsArray.count - 1)) {
[self.itemsArray insertObject:item atIndex:self.itemsArray.count - 1];
}
else{
self.itemsArray[indexPath.row] = item;
}
[self reloadCell];
}
3.自定义 Collectionviewcell 并实现删除按钮
先自定义一个 CDZCollectionViewCell,并用 xib 拖上一个 Imageview 用于展示图片,一个 button 用于点击关闭 Cell。并定义好解析 item 的方法。item 中设定一个 delBtnHidden 属性用于设定 Cell 里的 button 是否隐藏。
- (void)setItem:(CDZCollectionViewItem *)item{
// 解析需要的数据
self.imageView.image = item.image;
self.delButton.hidden = item.delBtnHidden;
}
并定义一个 Delegate 用于将按钮点击事件回传给 CollectionView 并删除数据。
@protocol CDZCollectionCellDelegate<NSObject>
- (void)didDelete:(UICollectionViewCell *)cell;
@end
@interface CDZCollectionViewCell : UICollectionViewCell
@property (strong, nonatomic) CDZCollectionViewItem *item;
@property (assign, nonatomic) id<CDZCollectionCellDelegate> delegate;
@end
并在点击按钮的事件中调用 Delegate,把 Cell 本身传回 TableView,从而找到 Cell 对应的 item。
- (IBAction)delCell:(UIButton *)sender{
if ([self.delegate respondsToSelector:@selector(didDelete:)]){
[self.delegate didDelete:self];
}
}
然后使 CollectionView 所在的 TableViewCell 遵守 CDZCollectionCellDelegate
。并和之前一样,在 CollectionView 的 Datasource 或 Delegate 中设置 Delegate。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
//......
cell.delegate = self;
//......
}
并调用 Cell 的 Delegate 的方法,通过 Cell 去找到对应的 indexPath。
- (void)didDelete:(UICollectionViewCell *)cell{
NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell];
[self.itemsArray removeObjectAtIndex:indexPath.row];
[self reloadCell];
}
第一个 Cell 是没有关闭按钮的,那么只要把第一个 item 的 delBthHidden 属性设为 YES 就可以了。
最后
所有源码和 Demo
如果您觉得有帮助,不妨给个 star 鼓励一下,欢迎关注&交流