iOS實現多個可變cell復雜界面的制作
在日常的開發中,有時會遇到內容塊比較多,且又可變的界面:
這個界面中有些內容塊是固定出現的,比如最上面的商品詳情圖片、商品名稱、價格等。而有些內容塊則是不一定出現的,比如促銷(顯然不是每個商品都有促銷)、已選規格(有的商品沒有規格)、店鋪信息(有的商品屬于自營,就沒有店鋪)等。還有些內容要根據情況進行變化,比如評論,這里最多列出4條評論,如果沒有評論,則顯示“暫無評論”且不顯示“查看所有評論”按鈕。
對于這樣的界面,相信很多人第一感覺會用TableView來做,因為中間要列出評論內容,這個用TableView的cell來填充比較合適。但如何處理評論內容之外的其他內容呢?我之前的做法是,評論內容之上的用HeaderView做,下面的用FooterView做,雖然最終實現了功能,但做起來十分麻煩。布局我是用Auto Layout來做的,由于Auto Layout本身的特點,代碼中就涉及很多判斷處理。比如“已選規格”塊,最開始的是有一個和“促銷”內容塊的頂部間距約束,但“促銷”內容塊不一定會有,就得根據情況,調整“已選規格”塊本身的約束(例如讓其頂部間距約束指向“價格”內容塊)。同樣,“促銷”內容塊本身也需要類似的處理,如果沒有“促銷”時,要隱藏自己,而隱藏自己最簡單的辦法,就是將自己的高度約束設置為0(因為它還有底部到“已選規格”的間距約束,不能隨意將自身移除,否則“已選規格”相關的約束得再進行調整)。
此外,還有一個麻煩的問題。界面剛進來的時候,是需要請求網絡數據,這時界面就要顯示成一個初始狀態,而顯然初始狀態有些內容塊是不應該顯示的,比如促銷,只有完成了數據請求,才能知道是否有促銷,有的話才顯示促銷內容;比如評論,初始時應該顯示成“暫無評論”,數據請求完成后,才顯示相應的內容。這樣,我們需要處理初始進入和數據請求完成兩種狀態下各個內容塊的顯示,十分復雜繁瑣。
總結來說,用TableView的 HeaderView + 評論內容cell + FooterView + Auto Layout 的方式會帶來如下問題:
- 約束本身需要依賴其他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 ()
@property (nonatomic, strong) NSMutableArray *> *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 *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;
}
上面的代碼進行了刪減,沒有處理所有類型。雖然稍嫌冗長,但是邏輯非常簡單,就是獲取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 *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 *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 *section3 = [NSMutableArray array];
NSArray *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數據的代碼順序即可
- 易于擴展增加新的內容塊。要增加新的內容塊,只需創建新的cell,在數據拼接時,增加拼接新cell類型的數據代碼,同樣在顯示的地方增加顯示新cell類型的代碼即可,幾乎不需要修改原有的邏輯
最后,附上Demo工程代碼(https://github.com/kelystor/MultipleVariantCell)。注意,這個工程是用XCode 8創建的,低版本的XCode可能運行會有問題(XCode 8的storyboard默認好像不兼容老版本),示例是基于iOS 8,如果要兼容老版本,請自行修改(主要是涉及cell自動算高的部分)。
來自:http://mobile.51cto.com/iphone-529922.htm