多布局 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 时数据出错的情况了。