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 時數據出錯的情況了。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容