iOS實現(xiàn)多個可變cell復(fù)雜界面的制作
在日常的開發(fā)中,有時會遇到內(nèi)容塊比較多,且又可變的界面:
這個界面中有些內(nèi)容塊是固定出現(xiàn)的,比如最上面的商品詳情圖片、商品名稱、價格等。而有些內(nèi)容塊則是不一定出現(xiàn)的,比如促銷(顯然不是每個商品都有促銷)、已選規(guī)格(有的商品沒有規(guī)格)、店鋪信息(有的商品屬于自營,就沒有店鋪)等。還有些內(nèi)容要根據(jù)情況進(jìn)行變化,比如評論,這里最多列出4條評論,如果沒有評論,則顯示“暫無評論”且不顯示“查看所有評論”按鈕。
對于這樣的界面,相信很多人***感覺會用TableView來做,因為中間要列出評論內(nèi)容,這個用TableView的cell來填充比較合適。但如何處理評論內(nèi)容之外的其他內(nèi)容呢?我之前的做法是,評論內(nèi)容之上的用HeaderView做,下面的用FooterView做,雖然最終實現(xiàn)了功能,但做起來十分麻煩。布局我是用Auto Layout來做的,由于Auto Layout本身的特點(diǎn),代碼中就涉及很多判斷處理。比如“已選規(guī)格”塊,最開始的是有一個和“促銷”內(nèi)容塊的頂部間距約束,但“促銷”內(nèi)容塊不一定會有,就得根據(jù)情況,調(diào)整“已選規(guī)格”塊本身的約束(例如讓其頂部間距約束指向“價格”內(nèi)容塊)。同樣,“促銷”內(nèi)容塊本身也需要類似的處理,如果沒有“促銷”時,要隱藏自己,而隱藏自己最簡單的辦法,就是將自己的高度約束設(shè)置為0(因為它還有底部到“已選規(guī)格”的間距約束,不能隨意將自身移除,否則“已選規(guī)格”相關(guān)的約束得再進(jìn)行調(diào)整)。
此外,還有一個麻煩的問題。界面剛進(jìn)來的時候,是需要請求網(wǎng)絡(luò)數(shù)據(jù),這時界面就要顯示成一個初始狀態(tài),而顯然初始狀態(tài)有些內(nèi)容塊是不應(yīng)該顯示的,比如促銷,只有完成了數(shù)據(jù)請求,才能知道是否有促銷,有的話才顯示促銷內(nèi)容;比如評論,初始時應(yīng)該顯示成“暫無評論”,數(shù)據(jù)請求完成后,才顯示相應(yīng)的內(nèi)容。這樣,我們需要處理初始進(jìn)入和數(shù)據(jù)請求完成兩種狀態(tài)下各個內(nèi)容塊的顯示,十分復(fù)雜繁瑣。
總結(jié)來說,用TableView的 HeaderView + 評論內(nèi)容cell + FooterView + Auto Layout 的方式會帶來如下問題:
- 約束本身需要依賴其他View的,而所依賴的View又是可變的內(nèi)容塊,會導(dǎo)致約束需要繁瑣的判斷修改增刪
- 需要處理初始進(jìn)入和數(shù)據(jù)請求完成兩種狀態(tài)的界面展示,使代碼更加復(fù)雜繁瑣
- 需要額外計算相應(yīng)內(nèi)容的高度,以更新HeaderView、FooterView的高度
可見,這種方式并不是理想的解決方案??赡苡腥藭f,那不要用Auto Layout,直接操作frame來布局就好,這樣或許能減少一些麻煩,但總體上并沒有減少復(fù)雜度。也有人說,直接用ScrollView來做,這樣的話,所有的內(nèi)容包括評論內(nèi)容的cell,都得自己手動拼接,可以想象這種做法也是比較麻煩的。所以,我們得另辟蹊徑,使用其他方法來達(dá)到目的。下面就為大家介紹一種比較簡便的做法,這種做法也是一個前同事分享給我的,我就借花獻(xiàn)佛,分享給大家。
我們還是用TableView來做這個界面,和之前不同的是,我們把每一個可變內(nèi)容塊做成一個獨(dú)立的cell,cell的粒度可以自行控制,比如可以用一個cell囊括商品圖片、標(biāo)題、副標(biāo)題、價格,也可以拆得更細(xì),圖片、標(biāo)題、副標(biāo)題、價格都各自對應(yīng)一個cell。這里我們選擇后者,因為圖片內(nèi)容塊,我們需要按屏幕寬度等比例拉伸;標(biāo)題、副標(biāo)題的文字內(nèi)容可能是一行,也可能是兩行,高度可變,用單獨(dú)的cell來控制會更簡單明了。
下面先定義好各種類型的cell:
- //基礎(chǔ)cell,這里為了演示簡便,定義這個cell,其他cell繼承自這個cell
- @interface MultipleVariantBasicTableViewCell : UITableViewCell
- @property (nonatomic, weak) UILabel *titleTextLabel;
- @end
- //滾動圖片
- @interface CycleImagesTableViewCell : MultipleVariantBasicTableViewCell
- @end
- //正標(biāo)題
- @interface MainTitleTableViewCell : MultipleVariantBasicTableViewCell
- @end
- //副標(biāo)題
- @interface SubTitleTableViewCell : MultipleVariantBasicTableViewCell
- @end
- //價格
- @interface PriceTableViewCell : MultipleVariantBasicTableViewCell
- @end
- // ...其他內(nèi)容塊的cell聲明
- // 各種內(nèi)容塊cell的實現(xiàn),這里為了演示簡便,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
- // ...其他內(nèi)容塊的cell實現(xiàn)
- // 評論內(nèi)容cell使用Auto Layout,配合iOS 8 TableView的自動算高,實現(xiàn)內(nèi)容自適應(yīng)
- @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
接下來就是重點(diǎn),就是如何來控制顯示哪些cell及cell顯示的數(shù)量。這一步如果處理不好,也會使開發(fā)變得復(fù)雜。如下面的方式:
- // 加載完數(shù)據(jù)
- self.cellCount = 0;
- if (存在促銷) {
- self.cellCount++;
- }
- if (存在規(guī)格) {
- self.cellCount++;
- }
- ......
如果以這種方式來記錄cell的數(shù)量,那么后續(xù)cell的展示、點(diǎn)擊判斷等都會很麻煩。這里我們采用的方式是,使用單獨(dú)的類(作為一種數(shù)據(jù)結(jié)構(gòu))來保存所要展示的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所需的信息,包括重用標(biāo)識、數(shù)據(jù)項、高度。接下來,我們就開始拼接cell信息。
- @interface ViewController ()
- @property (nonatomic, strong) NSMutableArray *> *tableSections;
- @end
- self.tableSections = [NSMutableArray array];
- /* 初始加載數(shù)據(jù)
- * 初始化時,只顯示滾動圖片、價格、評論頭、無評論
- */
- // 滾動圖片(寬高保持比例)
- 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]];
以上是初始狀態(tài)時要顯示的cell,我們在ViewController中聲明一個數(shù)組,用來存儲TableView各個section要顯示的cell信息。這里我們將cell分成不同的section,實際中,要不要分,分成幾個section都可以自行決定。初始狀態(tài)我們有兩個section,***個section用于顯示基本信息,第二個section用于顯示評論信息,這樣就完成了cell信息的拼接,接下來就是顯示:
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- // 這里可以通過判斷cellIdentifier來區(qū)分處理各種不同的cell,cell所需的數(shù)據(jù)從row.data上獲取
- SKRow *row = self.tableSections[indexPath.section][indexPath.row];
- if ([row.cellIdentifier isEqualToString:@"CycleImagesCellIdentifier"]) {
- CycleImagesTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:row.cellIdentifier forIndexPath:indexPath];
- NSArray *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 *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;
- }
上面的代碼進(jìn)行了刪減,沒有處理所有類型。雖然稍嫌冗長,但是邏輯非常簡單,就是獲取cell信息,根據(jù)重用標(biāo)識來區(qū)分不同類型的內(nèi)容塊,將數(shù)據(jù)處理后放到cell中展示。
例如,對于商品圖片,因為是滾動圖片,滾動圖片可以有多張,前面我們傳入的數(shù)據(jù)就是數(shù)組data:@[@"滾動圖片地址"]。后面獲取到數(shù)據(jù)后,cell.titleTextLabel.text = [urlStringArray componentsJoinedByString:@"\n"];,出于演示,商品圖片cell我們只放了一個Label,所以只是簡單的將地址信息分行顯示出來。在實際的開發(fā)中,可以放入一個圖片滾動顯示控件,并將圖片地址的數(shù)組數(shù)據(jù)傳給控件展示。
其他類型的cell處理也是大同小異,出于演示的原因,都只是簡單的數(shù)據(jù)處理展示。當(dāng)然,別忘了,設(shè)置一下TableView相關(guān)的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;
- }
這樣我們就完成了初始狀態(tài)時界面的展示
完成了cell的顯示處理,接下來我們來模擬一下網(wǎng)絡(luò)請求數(shù)據(jù)后,界面如何顯示所需的cell
- self.tableSections = [NSMutableArray array];
- NSMutableArray *section1 = [NSMutableArray array];
- // 滾動圖片(寬高保持比例)
- SKRow *cycleImagesRow = [[SKRow alloc] initWithCellIdentifier:@"CycleImagesCellIdentifier" data:@[@"滾動圖片地址1", @"滾動圖片地址2", @"滾動圖片地址3"] rowHeight:120*[UIScreen mainScreen].bounds.size.width / 320.f];
- // 主標(biāo)題
- SKRow *mainTitleRow = [[SKRow alloc] initWithCellIdentifier:@"MainTitleCellIdentifier" data:@"商品名稱" rowHeight:44];
- // 副標(biāo)題
- SKRow *subTitleRow = [[SKRow alloc] initWithCellIdentifier:@"SubTitleCellIdentifier" data:@"節(jié)日促銷,快來買啊" rowHeight:44];
- // 價格
- SKRow *priceRow = [[SKRow alloc] initWithCellIdentifier:@"PriceCellIdentifier" data:@(arc4random()) rowHeight:44];
- [section1 addObjectsFromArray:@[cycleImagesRow, mainTitleRow, subTitleRow, priceRow]];
- // 促銷(隨機(jī)出現(xiàn))
- if (arc4random() % 2 == 0) {
- SKRow *salePromotionRow = [[SKRow alloc] initWithCellIdentifier:@"SalePromotionCellIdentifier" data:@[@"促銷信息1", @"促銷信息2", @"促銷信息3"] rowHeight:44];
- [section1 addObject:salePromotionRow];
- }
- [self.tableSections addObject:section1];
- NSMutableArray *section2 = [NSMutableArray array];
- // 規(guī)格(隨機(jī)出現(xiàn))
- 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 *section3 = [NSMutableArray array];
- NSArray *commentArray = [NSMutableArray array];
- // 評論內(nèi)容數(shù)據(jù)(隨機(jī)出現(xiàn))
- if (arc4random() % 2 == 0) {
- commentArray = @[@"評論內(nèi)容1", @"評論內(nèi)容2", @"2016年6月,蘋果系統(tǒng)iOS 10正式亮相,蘋果為iOS 10帶來了十大項更新。2016年6月13日,蘋果開發(fā)者大會WWDC在舊金山召開,會議宣布iOS 10的測試版在2016年夏天推出,正式版將在秋季發(fā)布。2016年9月7日,蘋果發(fā)布iOS 10。iOS10正式版于9月13日(北京時間9月14日凌晨一點(diǎn))全面推送。", @"評論內(nèi)容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) {
- // 評論內(nèi)容需要自適應(yīng)高度,高度值指定為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數(shù)據(jù),有些不一定顯示的內(nèi)容塊,如促銷,則隨機(jī)判斷,如果顯示,將數(shù)據(jù)加入到section數(shù)組中[section1 addObject:salePromotionRow];。其他類型的cell也是類似的,不再贅述。要注意的是,評論內(nèi)容的文本可能有多行,我們將它的cell高設(shè)置為UITableViewAutomaticDimension:
[[SKRow alloc] initWithCellIdentifier:@"CommentContentCellIdentifier" data:commentString rowHeight:UITableViewAutomaticDimension];
由于評論內(nèi)容cell我們使用了Auto Layout,這樣就可以利用iOS 8 TableView的新特性,自動計算cell的高度。拼接完數(shù)據(jù)后,只要調(diào)用[self.tableView reloadData];讓TableView重新加載即可。
好了,這樣就大功告成
最終效果
使用上述方式制作這種內(nèi)容塊可變的界面雖然寫起來較為啰嗦,但有如下優(yōu)點(diǎn):
- 邏輯清晰簡單,易于理解,視圖間不存在像先前HeaderView + Auto Layout + FooterView那種復(fù)雜的依賴,內(nèi)容塊的顯示與否處理非常簡便
- 易于調(diào)整。例如調(diào)換內(nèi)容塊的順序,只要移動下拼湊cell數(shù)據(jù)的代碼順序即可
- 易于擴(kuò)展增加新的內(nèi)容塊。要增加新的內(nèi)容塊,只需創(chuàng)建新的cell,在數(shù)據(jù)拼接時,增加拼接新cell類型的數(shù)據(jù)代碼,同樣在顯示的地方增加顯示新cell類型的代碼即可,幾乎不需要修改原有的邏輯
***,附上Demo工程代碼(https://github.com/kelystor/MultipleVariantCell)。注意,這個工程是用XCode 8創(chuàng)建的,低版本的XCode可能運(yùn)行會有問題(XCode 8的storyboard默認(rèn)好像不兼容老版本),示例是基于iOS 8,如果要兼容老版本,請自行修改(主要是涉及cell自動算高的部分)。