iOS中TableView內嵌套CollectionView動態高度的實現(類似表單或圖片分享界面)

在有社交分享平臺屬性的app中,我們經常看見類似有tableview中多圖展示。不管是發布的表單界面中,還是社交動態的時間線的界面中,都需要根據圖片數量動態變化界面。最近剛好寫了一個這樣的界面,花了點時間寫了個Demo總結一下,希望可以幫助有需要的人。實現Demo效果如下圖。

collectionintable.gif

實現原理分析

初步思路是在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

謝謝觀看

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容