多布局 TableView 與復用 View 的問題
1. 多布局 TableView
UITableView 可以用來展示一系列的數據,即使數據表達樣式不盡相同,也可以將不同樣式的 TableViewCell 放在一個 TableView 中進行展示。
多布局 TableView 的基本思路如下:
- 用一個基礎 model 類表達抽象的 cell 數據,其中包含 type 字段作為區分布局的依據
- 為不同 type 的數據創建不同的 TableViewCell 類
- 給 TableView 注冊多個 TableViewCell 類和 CellReuseIdentifier
- 將一組 model 作為數據源,在 UITableViewController 返回 Cell 實例的方法中根據 type 創建和返回不同的 Cell 實例
2. 復用 View
其中要解決的主要問題是如果是與用戶有交互的(這種情況很常見),一旦 View 被復用就可能發生用戶輸入數據丟失或重復等問題,解決方法是實時將數據源的數據進行同步更新,然后對于復用的 view 保證從數據源獲取最新的數據。
在這里以一個通訊錄編輯頁面的例子作為說明,我們要在一個 TableView 中加入包括 UITextField 、分割單元、選擇器等在內的多種布局 Cell。
首先定義一個數據模型 Model
typedef enum
{
TextFieldType,
SeparatorType,
SelectorType
}MenuType;
@interface PhoneBookDetailMenuModel : NSObject
@property(strong, nonatomic) NSString *name;
@property(assign, nonatomic) MenuType cellType;
@property(strong, nonatomic) NSString *textInfo;
@end
name 用于區分各個 model,同時作為 UITextField 的placeholder。cellType 是一個類型為 MenuType 的枚舉,包括三種枚舉值。textInfo 是 textField 的 text 值,初始為空。
接下來定義了兩種 Cell 布局。
-
TextFieldCell
#import <UIKit/UIKit.h> @interface TextFieldCell : UITableViewCell @property(strong, nonatomic) UITextField *input; @end #import "TextFieldCell.h" #import "Masonry.h" #define kWidth [UIScreen mainScreen].bounds.size.width #define kHeight [UIScreen mainScreen].bounds.size.height @implementation TextFieldCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { CGRect frame = CGRectMake(16, 16, kWidth - 32, 32); _input = [[UITextField alloc] initWithFrame:frame]; _input.borderStyle = UITextBorderStyleRoundedRect; [self.contentView addSubview:_input]; } return self; } @end
-
SelectorCell
#import <UIKit/UIKit.h> @interface SelectorCell : UITableViewCell @property(strong, nonatomic) UILabel *titleLabel; @property(strong, nonatomic) UIButton *selector; @end #import "SelectorCell.h" #define kWidth [UIScreen mainScreen].bounds.size.width #define kHeight [UIScreen mainScreen].bounds.size.height @implementation SelectorCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { CGRect labelFrame = CGRectMake(16, 16, kWidth/2, 32); _titleLabel = [[UILabel alloc] initWithFrame:labelFrame]; _titleLabel.textAlignment = NSTextAlignmentLeft; [self.contentView addSubview:_titleLabel]; _selector = [[UIButton alloc] initWithFrame:CGRectMake(kWidth/2, 16, kWidth/2 - 16, 32)]; _selector.contentHorizontalAlignment = NSTextAlignmentRight; [self.contentView addSubview:_selector]; } return self; } - (void)awakeFromNib { [super awakeFromNib]; // Initialization code } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } @end
然后是對 TableViewController 的初始化工作。
_menuModelArray = [NSMutableArray arrayWithCapacity:10];
self.tableView = [[UITableView alloc] initWithFrame:self.view.frame style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
PhoneBookDetailMenuModel *nameModel = [[PhoneBookDetailMenuModel alloc] init];
nameModel.name = @"name";
nameModel.cellType = TextFieldType;
nameModel.textInfo = @"";
[_menuModelArray addObject:nameModel];
PhoneBookDetailMenuModel *phoneNumberModel = [[PhoneBookDetailMenuModel alloc] init];
phoneNumberModel.name = @"phoneNumber";
phoneNumberModel.cellType = TextFieldType;
phoneNumberModel.textInfo = @"";
[_menuModelArray addObject:phoneNumberModel];
PhoneBookDetailMenuModel *separatorModel = [[PhoneBookDetailMenuModel alloc] init];
separatorModel.name = @"separator";
separatorModel.cellType = SeparatorType;
separatorModel.textInfo = @"";
[_menuModelArray addObject:separatorModel];
PhoneBookDetailMenuModel *addressModel = [[PhoneBookDetailMenuModel alloc] init];
addressModel.name = @"address";
addressModel.cellType = TextFieldType;
addressModel.textInfo = @"";
[_menuModelArray addObject:addressModel];
PhoneBookDetailMenuModel *emailModel = [[PhoneBookDetailMenuModel alloc] init];
emailModel.name = @"email";
emailModel.cellType = TextFieldType;
emailModel.textInfo = @"";
[_menuModelArray addObject:emailModel];
PhoneBookDetailMenuModel *remarksModel = [[PhoneBookDetailMenuModel alloc] init];
remarksModel.name = @"remarks";
remarksModel.cellType = TextFieldType;
remarksModel.textInfo = @"";
[_menuModelArray addObject:remarksModel];
PhoneBookDetailMenuModel *genderModel = [[PhoneBookDetailMenuModel alloc] init];
genderModel.name = @"Gender";
genderModel.cellType = SelectorType;
genderModel.textInfo = @"Male";
[_menuModelArray addObject:genderModel];
PhoneBookDetailMenuModel *birthDateModel = [[PhoneBookDetailMenuModel alloc] init];
birthDateModel.name = @"BirthDate";
birthDateModel.cellType = SelectorType;
birthDateModel.textInfo = @"1990-01-01";
[_menuModelArray addObject:birthDateModel];
PhoneBookDetailMenuModel *ageModel = [[PhoneBookDetailMenuModel alloc] init];
ageModel.name = @"Age";
ageModel.cellType = SelectorType;
ageModel.textInfo = [NSString stringWithFormat:@"%ld", [self calculateAge:birthDateModel.textInfo]];
[_menuModelArray addObject:ageModel];
for (int i = 0; i < 20; i++)
{
PhoneBookDetailMenuModel *model = [[PhoneBookDetailMenuModel alloc] init];
model.name = [NSString stringWithFormat:@"Test%d", i];
model.cellType = TextFieldType;
model.textInfo = @"";
[_menuModelArray addObject:model];
}
[self.tableView registerClass:[TextFieldCell class] forCellReuseIdentifier:textFieldIdentifier];
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:separatorIdentifier];
[self.tableView registerClass:[SelectorCell class] forCellReuseIdentifier:selectorIdentifier];
這里我們去除了 TableView 默認的分割線,為了測試還加入了20個測試的 TextField。
接下來要對數據源和委托方法進行復寫,重點是其中的 (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MenuType type = [_menuModelArray[indexPath.row] cellType];
if (type == TextFieldType) //輸入框類型
{
TextFieldCell *cell = [tableView dequeueReusableCellWithIdentifier:textFieldIdentifier forIndexPath:indexPath];
cell.input.placeholder = @""; //清除可能存在的數據
cell.input.text = @""; //清除可能存在的數據
cell.selectionStyle = UITableViewCellSelectionStyleNone;
if ([[_menuModelArray[indexPath.row] textInfo] isEqualToString:@""])
{
cell.input.placeholder = [_menuModelArray[indexPath.row] name];
}
else
{
cell.input.text = [_menuModelArray[indexPath.row] textInfo];
}
cell.input.tag = indexPath.row; //按照 tag 值在 UIControlEventEditingChanged 監聽函數中更新對應的 model
[cell.input addTarget:self action:@selector(inputChanged:) forControlEvents:UIControlEventEditingChanged];
return cell;
}
if (type == SeparatorType) //分割單元
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:separatorIdentifier forIndexPath:indexPath];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
return cell;
}
if (type == SelectorType) //選擇器單元
{
···
···
return cell;
}
return [UITableViewCell new];
}
首先根據 indexPath 的 row 值可以獲取到數據源數組中對應的model,從而得知 type 值,根據 type 值生成或從已有的 view 中復用對應的 cell,然后清除其中數據。
清除數據的步驟必須要做,否則就會出問題。比如這里,接下來會按照 model 的 textInfo 屬性確定是給 cell 的 TextField 設置 placeholder 還是 text,但是如果復用的 view 本身就有 text,再賦值 placeholder 是不會清除 text 的,就會發生數據復用的問題。
清除數據后設置 TextField 的值,然后要對 Cell 的 TextField 設置 tag 值,從而按照 tag 值在 UIControlEventEditingChanged 監聽函數中更新對應的 model。
監聽函數 inputChanged 如下
- (void)inputChanged:(UITextField *)targetField
{
((PhoneBookDetailMenuModel *)_menuModelArray[targetField.tag]).textInfo = targetField.text;
}
主要是根據 tag 值從數據源數組中找到對應的 model,然后更新其中的 textInfo 屬性,從而保證數據源數據是最新的,這樣就不會出現復用 view 時數據出錯的情況了。