在有社交分享平臺屬性的app中,我們經常看見類似有tableview中多圖展示。不管是發布的表單界面中,還是社交動態的時間線的界面中,都需要根據圖片數量動態變化界面。最近剛好寫了一個這樣的界面,花了點時間寫了個Demo總結一下,希望可以幫助有需要的人。實現Demo效果如下圖。
實現原理分析
初步思路是在tableview的cell中內嵌一個collecitonview,collectionview高度動態變化并是collectionview所在的tableview的cell的高度動態變化。總結起來我們需要這幾件事:
1.實現一個tableview并自定義一種tableviewcell并實現高度自適應
2.在tableviewcell中實現collectionview并實現高度動態變化
3.自定義collectionviewcell中實現按鈕點擊事件(如刪除,跳轉),數據展示操作
1.實現tableview和自定義tableviewcell
tableview就很簡單,storyboard或者代碼寫一下都可以。實現數據源協議啥的,很普通。要實現tableviewcell的高度自適應,一般來說有兩種方式,一種是用iOS7后支持的cell的estimatedRowHeight和iOS8后支持的self-sizing cells(兩者差不多,iOS8更完善一些),另一種是用孫源大神的第三方開源庫,可以看這篇文章,兩者共同之處都是需要設置cell里contenview的元素對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的大小就好了。在tableviewcellawakeFromNib
方法中,我們讓collectionviewreloadData
更新一下高度布局約束為collectionview的真實高度并調用剛才自定義的delegate去讓tableviewreloadData
從而讓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里等操作(比如調用相冊等)。注意當點擊最后一個collectioncell時,要在它的后面插入一個數據,也就是說,最后一個總是會保持在最后。
- (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鼓勵一下,歡迎關注&交流
有任何問題歡迎評論私信或者提issue
QQ:757765420
Email:nemocdz@gmail.com
Github:Nemocdz
微博:@Nemocdz