1. 程式人生 > >ios中在tableviewcell中有textfield

ios中在tableviewcell中有textfield

前言

問題背景:自定義cell中有一個UITextField型別的子控制元件。我們經常要在tableView中拿到某個cell內textField的文字內容進行一些操作。比如某些app的註冊介面就是以tableView的形式存在的,註冊時往往需要註冊姓名、暱稱、郵箱、地址、聯絡方式等資訊。然後點選註冊或者提交,這些資訊就會被提交到遠端伺服器。有人說,註冊頁面就那麼固定的幾行cell,沒必要搞得那麼複雜,完全可以用靜態cell實現。但還有一些情況,當前頁面的tableView的cell的行數是不確定的(比如當前頁面顯示多好行cell由上一個頁面決定或者由使用者決定),這種情況下不太適合使用靜態cell。也不能夠通過分支語句的方式一一枚舉出各個case。所以需要一中通用的動態的方法。那麼我們怎麼在tableView中準確的拿到每一行cell中textField的text呢?以下我將要分四個方法分別介紹並逐一介紹他們的優缺點,大家可以在開發中根據實際情況有選擇的採用不同的方法。
如下圖,就是我之前開發的一個app中用xib描述的一個cell,當用戶點選“註冊”或者“提交”button時候,我需要在控制器中拿到諸如“法人姓名”這一類的資訊:

cellWithXib.png

四個方法告訴你如何在tableView中拿到每一個cell中的textField.text
四個方法分別如下:

  • 通過控制器的textField屬性來拿到每一個cell內textField.text
  • 通過系統預設傳送的通知來拿到每一個cell內textField.text
  • 通過自定義的通知來拿到每一個cell內textField.text
  • 通過block來拿到每一個cell內textField.text

方法一(方法1請略過)

1.cell的.h檔案宣告一個IBOutlet的屬性,使其和xib描述的cell中的textField進行關聯。
1.在tableViewController.m的類擴充套件中宣告為每一個cell的textField都宣告一個UITextField型別的屬性,一一對應。
2.在cellForRowAtIndexPath:資料來源方法中給控制器的每個UITextField型別屬性賦值為cell.textField。

TableViewCell.h檔案中的contentTextField引用xib中的textField:

#import <UIKit/UIKit.h>

@interface TableViewCell : UITableViewCell
/**
 *  cell的標題
 */
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;

/**
 *  cell的文字框
 */
@property (weak, nonatomic) IBOutlet UITextField *contentTextField;
@end

控制器中宣告UITextField型別的屬性。

@interface YQBInfoViewController ()
/**
 *  標題
 */
@property(nonatomic, strong) NSArray *titles;
/**
 *  佔位文字
 */
@property(nonatomic, strong) NSArray *placeHolders;

/**
 *  姓名
 */
@property(nonatomic, weak) UITextField *nameTextField;

/**
 *  年齡
 */
@property(nonatomic, weak) UITextField *ageTextField;

/**
 *  地址
 */
@property(nonatomic, weak) UITextField *addressTextField;
@end

資料來源方法cellForRowAtIndexPath:中給控制器的UITextField型別屬性賦值。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    // 在這裡把每個cell的textField 賦值給 事先宣告好的UITextField型別的屬性
    // 以後直接操作控制器的這些屬性就可以拿到每個textField的值
    switch (indexPath.row) {
        case 0:
            // 姓名
            self.nameTextField = cell.contentTextField;
            break;
        case 1:
            // 年齡
            self.ageTextField = cell.contentTextField;
            break;
        case 2:
            // 地址
            self.addressTextField = cell.contentTextField;
            break;
        default:
            break;
    }
    
    return cell;
}

但是,這個方法還是有一些小問題,因為cell被重用時,存在存在的內容錯亂的現象。有人說,因為我們在cellForRowAtIndexPath用一個UITextField屬性引用了cell的contentTextfield,我們可以在willDisplayCell:方法中對cell的contentTextField的內容再次配置回來。而事實上,因為cell此時被重用了,所以,我們的tableViewController的那些分別指向每一行cell的UITextField的屬性此時也指向了其他行。所以,這個方法對於cell存在重用的情況是不適合的!

方法二(傳送系統通知)

我們知道UITextField內容改變時會發送通知。與UITextField相關的通知有三個,如下:

UIKIT_EXTERN NSString *const UITextFieldTextDidBeginEditingNotification;
UIKIT_EXTERN NSString *const UITextFieldTextDidEndEditingNotification;
UIKIT_EXTERN NSString *const UITextFieldTextDidChangeNotification;

1.我們只需要讓tableVeiw控制器註冊UITextFieldTextDidChangeNotification/UITextFieldTextDidEndEditingNotification通知。
2.在資料來源方法cellForRowAtIndexPath:中對cell.textField.tag賦值為indexPath.row。這樣就可以區分每一行的textField。
3.然後在監聽到通知後呼叫的方法中,根據textField.tag拿到textField的內容。

但是,問題來了,如果tableView是grouped樣式的呢?這樣就有可能存在兩個textField具有相同的tag!所以,以上提供的思路只適用於plained樣式的tableView。grouped樣式的tableView建議用下面的方法。
解決方法:自定義textField,給textField新增NSIndexPath型別的屬性indexPath。我們這次給textField的indexPath賦值而不是tag。這樣就可以在監聽到通知後呼叫的方法中,根據indexPath來區分不同的section和row。

自定義UITextField

#import <UIKit/UIKit.h>

@interface CustomTextField : UITextField
/**
 *  indexPath屬性用於區分不同行cell
 */
@property (strong, nonatomic) NSIndexPath *indexPath;
@end

注意:如果你自定義的cell是用xib描述的,不要忘記給cell的textField指定型別為你自定義的textField,此例中我自定義的是CustomTextField,如下圖:

Snip20160503_2.png

控制器註冊通知

// 註冊通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentTextFieldDidEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];

給自定義的textField的indexPath屬性賦值

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    AliyunSalesUnifiedEditCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
    
    // 如果不止一個section,那麼傳遞indexPath.row有可能衝突
    // cell.contentTextField.tag = indexPath.row;

    // 所以傳遞indexPath,相當於把section也傳遞給contentTextField
    cell.contentTextField.indexPath = indexPath;
    return cell;
}

監聽到通知後呼叫的方法

// 在這個方法中,我們就可以通過自定義textField的indexPath屬性區分不同行的cell,然後拿到textField.text
- (void)contentTextFieldDidEndEditing:(NSNotification *)noti {
    CustomTextField *textField = noti.object;
    if (textField.indexPath.section == 0) {
        NSString *text = textField.text;
        NSInteger row =textField.indexPath.row;
        
        if (text && text.length) {
            [self.contents replaceObjectAtIndex:row withObject:text];
        }
    } else if (textField.indexPath.section == 1) {
        // 同上,請自行腦補
    } else if (textField.indexPath.section == 2) {
        // 同上,請自行腦補
    } else {
        // 同上,請自行腦補
    }
}

切記:對於cell的重用,當在willDisplayCell方法中重新配置cell時候,有if,就必須有else。因為之前螢幕上出現的cell離開螢幕被快取起來時候,cell上的內容並沒有清空,當cell被重用時,系統並不會給我們把cell上之前配置的內容清空掉,所以我們在else中對contentTextField內容進行重新配置或者清空(根據自己的業務場景而定)

方法三(傳送自定義通知)

其實方法三和方法二很像,都需要給自定義的textField新增indexPath屬性,也需要傳送通知,然後在通知中心對這個通知註冊監聽。區別在於,方法二傳送的是系統自帶的通知UITextFieldTextDidEndEditingNotification,而方法三將要傳送自定義通知。
1>給CustomTextField新增indexPath屬性。
2>給自定義cell新增CustomTextField型別contentTextField屬性。
3>cell遵守UITextFieldDelegate協議,成為textField屬性的delegate。
4>cell實現協議方法-textFieldDidEndEditing:(UITextField *)textField
5>textFieldDidEndEditing:協議方法中傳送一個自定義的通知,並且把textField.text通過userInfo字典發出去。

具體實現程式碼:

給CustomTextField新增indexPath屬性

#import <UIKit/UIKit.h>

@interface CustomTextField : UITextField
/**
 *  indexPath屬性用於區分不同的cell
 */
@property (strong, nonatomic) NSIndexPath *indexPath;
@end

給自定義cell新增CustomTextField型別contentTextField屬性

#import <UIKit/UIKit.h>
@class CustomTextField;

@interface TableViewCell : UITableViewCell
/**
 *  cell的標題
 */
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;

/**
 *  cell的文字框
 */
@property (weak, nonatomic) IBOutlet CustomTextField *contentTextField;

@end

遵守協議,設定delegate,實現協議方法

#import "TableViewCell.h"
#import "CustomTextField.h"

@interface TableViewCell ()<UITextFieldDelegate>

@end

@implementation TableViewCell
- (void)awakeFromNib {
    [super awakeFromNib];
    self.selectionStyle = UITableViewCellSelectionStyleNone;
    self.contentTextField.delegate = self;
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 使contentTextField聚焦變成第一響應者
    [self.contentTextField becomeFirstResponder];
    
}

#pragma mark - UITextFieldDelegate
- (void)textFieldDidEndEditing:(UITextField *)textField
{
    NSDictionary *userInfo = @{
                               @"textFieldText":self.contentTextField.text
                               };
    [[NSNotificationCenter defaultCenter] postNotificationName:@"CustomTextFieldDidEndEditingNotification" object:self.contentTextField userInfo:userInfo];
}

6>控制器註冊並監聽該通知
7>在監聽到通知的方法中通過userInfo拿到textField的text屬性
8>- (void)viewWillDisappear:(BOOL)animated方法中移除監聽
9>完畢

註冊通知

// 如果不能保證控制器的dealloc方法肯定會被呼叫,不要在viewDidLoad方法中註冊通知。
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
   
    // 注意:此處監聽的通知是:UITextFieldTextDidEndEditingNotification,textField結束編輯傳送的通知,textField結束編輯時才會傳送這個通知。
    // 想實時監聽textField的內容的變化,你也可以註冊這個通知:UITextFieldTextDidChangeNotification,textField值改變就會發送的通知。
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cellTextFieldDidEndEditing:) name:@"CustomTextFieldDidEndEditingNotification" object:nil];
    
    //    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contentTextFieldDidEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
}

移除通知


- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    // 在這個方法裡移除通知,因為:
    // 防止控制器被強引用導致-dealloc方法沒有呼叫
    // 其他介面也有textField,其他介面的textField也會發送同樣的通知,導致頻繁的呼叫監聽到通知的方法,而這些通知是這個介面不需要的,所以在檢視將要消失的時候移除通知 同樣,在檢視將要顯示的時候註冊通知
    [[NSNotificationCenter defaultCenter] removeObserver:self name:@"CustomTextFieldDidEndEditingNotification" object:nil];
}

接收到通知回撥方法

// 接收到註冊監聽的通知後呼叫
- (void)cellTextFieldDidEndEditing:(NSNotification *)noti {
    CustomTextField *textField = noti.object;
    if (!textField.indexPath) {
        return;
    }
    
    NSString *userInfoValue = [noti.userInfo objectForKey:@"textFieldText"];
    NSLog(@"text:%@,userInfoValue:%@",textField.text,userInfoValue);
    // 如果涉及到多個section,可以使用二維陣列,此處不再贅述
    if (textField.indexPath.section == 0) {
        [self.contents replaceObjectAtIndex:textField.indexPath.row withObject:userInfoValue];

    } else if (textField.indexPath.section == 1) {
        // 同上,請自行腦補
    } else if (textField.indexPath.section == 2) {
        // 同上,請自行腦補
    } else {
        // 同上,請自行腦補
    }
}

切記:對於cell的重用,當在willDisplayCell方法中重新配置cell時候,有if,就必須有else。因為之前螢幕上出現的cell離開螢幕被快取起來時候,cell上的內容並沒有清空,當cell被重用時,系統並不會給我們把cell上之前配置的內容清空掉,所以我們在else中對contentTextField內容進行重新配置或者清空(根據自己的業務場景而定)

以下是方法三的demo地址
方法三相對於方法二的好處在於:方法三傳送的是自定義通知,而方法二傳送的是系統自帶的通知。
因為專案開發中,受專案複雜度影響,難免會出現不同的控制器介面都會有UITextField型別(或者其子型別)的物件而沒有釋放,當textField開始編輯、內容發生改變、結束編輯時,都會發送相同的通知。此時如果我們採用監聽系統自帶的通知的方法,就有可能監聽到我們不需要的改變從而影響了業務資料。
舉個例子:A和B控制器都是UITableViewController型別的物件,A、B控制器介面上都有UITextField型別(或者其子型別)的子控制元件。並且A、B控制器都註冊了系統自帶的UITextField的通知UITextFieldTextDidChangeNotification,且監聽到通知後都會呼叫各自的contentTextFieldTextDidChange:方法。當A控制器pushB控制器後,我們在B控制器介面上的TextField編輯內容,A控制器此時也監聽了該通知,所以,A控制器上的contentTextFieldTextDidChange:方法也會被呼叫。這是我們不想得到的,所以,採用自定義通知的方法可以避免這一問題。
當然,我們也可以在viewWillAppear:方法中註冊通知,然後在viewWillDisAppear:方法中移除通知,這樣同樣可以避免這一為題。
另外,值得提醒的是,如果我們不能保證控制器被pop時肯定會呼叫dealloc方法,那麼建議在控制器的viewWillDisAppear:方法中移除通知,而非dealloc方法中移除。否則,使用者反覆push、pop控制器時,控制器可能會註冊多份相同的通知。

方法四(使用block)

1>給cell新增一個block屬性,該block屬性帶有一個NSString *型別的引數。
2>給cell的textField新增target,觸發方法的事件是UIControlEventEditingChanged
3>textField觸發的方法中呼叫cell的這個block屬性,並把contentTextField.text作為block的引數傳進去
4>資料來源方法cellForRowAtIndexPath:中對cell的block屬性賦值(也就是拿到cell.contentTextField.text)
5>資料來源方法willDisplayCell:中對cell重新配置。

給cell新增一個block屬性

#import <UIKit/UIKit.h>

@interface TableViewCell : UITableViewCell
/**
 *  block 引數為textField.text
 */

@property (copy, nonatomic) void(^block)(NSString *);
/**
 *  cell的標題
 */

@property (weak, nonatomic) IBOutlet UILabel *titleLabel;

/**
 *  cell的文字框
 */
@property (weak, nonatomic) IBOutlet UITextField *contentTextField;

@end

給textField addTarget
在事件觸發方法中呼叫block並傳遞引數

#import "TableViewCell.h"

@interface TableViewCell ()

@end

@implementation TableViewCell

- (void)awakeFromNib {
    [super awakeFromNib];
    self.selectionStyle = UITableViewCellSelectionStyleNone;
    [self.contentTextField addTarget:self action:@selector(textfieldTextDidChange:) forControlEvents:UIControlEventEditingChanged];
// 注意:不是 UIControlEventValueChanged
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.contentTextField becomeFirstResponder];
}

#pragma mark - private method
- (void)textfieldTextDidChange:(UITextField *)textField
{
    self.block(self.contentTextField.text);
}
@end

在cellforRowAtIndexPath:方法中為每個cell的block賦值

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    TableViewCell *customCell = [tableView dequeueReusableCellWithIdentifier:ID];
    
    __weak typeof(self) weakSelf = self;
    if (indexPath.section == 0) {
        customCell.block = ^(NSString * text) {
            // 更新資料來源
            [weakSelf.contents replaceObjectAtIndex:indexPath.row withObject:text];
        };
    } else if (indexPath.section == 1) {
        // 同上,請自行腦補
    } else {
        // 同上,請自行腦補
    }
    return customCell;
}

在willDisplayCell:方法中對cell進行配置:

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell *customCell = (TableViewCell *)cell;
    customCell.titleLabel.text = self.titles[indexPath.row];
    customCell.contentTextField.placeholder = self.placeHolders[indexPath.row];
    if (indexPath.section == 0) {
        customCell.contentTextField.text = [self.contents objectAtIndex:indexPath.row];
        // 必須有else!
    } else {
        // 切記:對於cell的重用,有if,就必須有else。因為之前螢幕上出現的cell離開螢幕被快取起來時候,cell上的內容並沒有清空,當cell被重用時,系統並不會給我們把cell上之前配置的內容清空掉,所以我們在else中對contentTextField內容進行重新配置或者清空(根據自己的業務場景而定)
        customCell.contentTextField.text = [NSString stringWithFormat:@"第%ld組,第%ld行",indexPath.section,indexPath.row];
    }
}

切記:對於cell的重用,當在willDisplayCell方法中重新配置cell時候,有if,就必須有else。因為之前螢幕上出現的cell離開螢幕被快取起來時候,cell上的內容並沒有清空,當cell被重用時,系統並不會給我們把cell上之前配置的內容清空掉,所以我們在else中對contentTextField內容進行重新配置或者清空(根據自己的業務場景而定)

以下是方法四的demo地址
方法四相對於方法二和方法三的好處在於:方法四沒有采用通知的方式來獲取contentTextField.text,而是採用靈活的block。並且方法四也無需自定義textField。

方法五(使用delegate實現)

方法五和方法四很像,只不過方法五采用了delegate方式,更好的做到了解耦。
0>和方法二、方法三一樣,cell的textField屬性都需要使用自定義型別,因為我們需要給textField繫結indexPath屬性。
1>給cell制定一份協議,協議中有一個方法,帶有兩個引數,一個是textField的text,另一個是indexPath。同時給cell新增一個delegate屬性。
2>給cell的textField新增target,觸發方法的事件是UIControlEventEditingChanged
3>textField觸發的方法中呼叫cell的協議方法,並把contentTextField.indexPath作為協議方法的引數傳進去
4>資料來源方法cellForRowAtIndexPath:中對cell的indexPath賦值為當前的indexPath。對cell的delegate賦值為當前controller
5>控制器實現cell的協議方法,在協議方法裡可以拿到textField的文字。
6>在tableView:willDisplayCell:forRowAtIndexPath:方法內重新整理tableView。

#import <UIKit/UIKit.h>
@class CustomTextField;
@protocol CustomCellCellDelegate <NSObject>

@required
// cell 的contentTextField的文字發生改變時呼叫
- (void)contentDidChanged:(NSString *)text forIndexPath:(NSIndexPath *)indexPath;

@end
@interface TableViewCell : UITableViewCell
/**
 *  cell的標題
 */
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;

/**
 *  cell的文字框
 */
@property (weak, nonatomic) IBOutlet CustomTextField *contentTextField;
/**
 *  delegate
 */
@property (weak, nonatomic) id<CustomCellCellDelegate> delegate;

cell.m檔案


- (void)awakeFromNib {
    [super awakeFromNib];
    self.selectionStyle = UITableViewCellSelectionStyleNone;
    [self.contentTextField addTarget:self action:@selector(contentDidChanged:) forControlEvents:UIControlEventEditingChanged];
}

- (void)contentDidChanged:(id)sender {
    // 呼叫代理方法,告訴代理,哪一行的文字發生了改變
    if (self.delegate && [self.delegate respondsToSelector:@selector(contentDidChanged:forIndexPath:)]) {
        [self.delegate contentDidChanged:self.contentTextField.text forIndexPath:self.contentTextField.indexPath];
    }
}

controller.m


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    cell.contentTextField.indexPath = indexPath;
    cell.delegate = self;
    return cell;
}
// cell的代理方法中拿到text進行儲存
- (void)contentDidChanged:(NSString *)text forIndexPath:(NSIndexPath *)indexPath {
    [self.contents replaceObjectAtIndex:indexPath.row withObject:text];
}
// 更新UI
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
    TableViewCell *customCell = (TableViewCell *)cell;
    customCell.titleLabel.text = self.titles[indexPath.row];
    customCell.contentTextField.placeholder = self.placeHolders[indexPath.row];
    customCell.contentTextField.text = self.contents[indexPath.row];
}