008-多布局TableView与复用View的问题

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容