前言
問題背景:自定義cell中有一個UITextField類型的子控件。我們經(jīng)常要在tableView中拿到某個cell內(nèi)textField的文本內(nèi)容進行一些操作。比如某些app的注冊界面就是以tableView的形式存在的,注冊時往往需要注冊姓名、昵稱、郵箱、地址、聯(lián)系方式等信息。然后點擊注冊或者提交,這些信息就會被提交到遠程服務(wù)器。有人說,注冊頁面就那么固定的幾行cell,沒必要搞得那么復(fù)雜,完全可以用靜態(tài)cell實現(xiàn)。但還有一些情況,當(dāng)前頁面的tableView的cell的行數(shù)是不確定的(比如當(dāng)前頁面顯示多好行cell由上一個頁面決定或者由用戶決定),這種情況下不太適合使用靜態(tài)cell。也不能夠通過分支語句的方式一一枚舉出各個case。所以需要一中通用的動態(tài)的方法。那么我們怎么在tableView中準(zhǔn)確的拿到每一行cell中textField的text呢?以下我將要分四個方法分別介紹并逐一介紹他們的優(yōu)缺點,大家可以在開發(fā)中根據(jù)實際情況有選擇的采用不同的方法。
如下圖,就是我之前開發(fā)的一個app中用xib描述的一個cell,當(dāng)用戶點擊“注冊”或者“提交”button時候,我需要在控制器中拿到諸如“法人姓名”這一類的信息:

四個方法告訴你如何在tableView中拿到每一個cell中的textField.text
四個方法分別如下:
- 通過控制器的textField屬性來拿到每一個cell內(nèi)textField.text
- 通過系統(tǒng)默認(rèn)發(fā)送的通知來拿到每一個cell內(nèi)textField.text
- 通過自定義的通知來拿到每一個cell內(nèi)textField.text
- 通過block來拿到每一個cell內(nèi)textField.text
方法一(方法1請略過)
1.cell的.h文件聲明一個IBOutlet的屬性,使其和xib描述的cell中的textField進行關(guān)聯(lián)。
1.在tableViewController.m的類擴展中聲明為每一個cell的textField都聲明一個UITextField類型的屬性,一一對應(yīng)。
2.在cellForRowAtIndexPath:數(shù)據(jù)源方法中給控制器的每個UITextField類型屬性賦值為cell.textField。
TableViewCell.h文件中的contentTextField引用xib中的textField:
#import <UIKit/UIKit.h>
@interface TableViewCell : UITableViewCell
/**
* cell的標(biāo)題
*/
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
/**
* cell的文本框
*/
@property (weak, nonatomic) IBOutlet UITextField *contentTextField;
@end
控制器中聲明UITextField類型的屬性。
@interface YQBInfoViewController ()
/**
* 標(biāo)題
*/
@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
數(shù)據(jù)源方法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被重用時,存在存在的內(nèi)容錯亂的現(xiàn)象。有人說,因為我們在cellForRowAtIndexPath用一個UITextField屬性引用了cell的contentTextfield,我們可以在willDisplayCell:方法中對cell的contentTextField的內(nèi)容再次配置回來。而事實上,因為cell此時被重用了,所以,我們的tableViewController的那些分別指向每一行cell的UITextField的屬性此時也指向了其他行。所以,這個方法對于cell存在重用的情況是不適合的!
以下是方法一的demo地址
方法二(發(fā)送系統(tǒng)通知)
我們知道UITextField內(nèi)容改變時會發(fā)送通知。與UITextField相關(guān)的通知有三個,如下:
UIKIT_EXTERN NSString *const UITextFieldTextDidBeginEditingNotification;
UIKIT_EXTERN NSString *const UITextFieldTextDidEndEditingNotification;
UIKIT_EXTERN NSString *const UITextFieldTextDidChangeNotification;
1.我們只需要讓tableVeiw控制器注冊UITextFieldTextDidChangeNotification/UITextFieldTextDidEndEditingNotification通知。
2.在數(shù)據(jù)源方法cellForRowAtIndexPath:中對cell.textField.tag賦值為indexPath.row。這樣就可以區(qū)分每一行的textField。
3.然后在監(jiān)聽到通知后調(diào)用的方法中,根據(jù)textField.tag拿到textField的內(nèi)容。
但是,問題來了,如果tableView是grouped樣式的呢?這樣就有可能存在兩個textField具有相同的tag!所以,以上提供的思路只適用于plained樣式的tableView。grouped樣式的tableView建議用下面的方法。
解決方法:自定義textField,給textField添加NSIndexPath類型的屬性indexPath。我們這次給textField的indexPath賦值而不是tag。這樣就可以在監(jiān)聽到通知后調(diào)用的方法中,根據(jù)indexPath來區(qū)分不同的section和row。
自定義UITextField
#import <UIKit/UIKit.h>
@interface CustomTextField : UITextField
/**
* indexPath屬性用于區(qū)分不同行cell
*/
@property (strong, nonatomic) NSIndexPath *indexPath;
@end
注意:如果你自定義的cell是用xib描述的,不要忘記給cell的textField指定類型為你自定義的textField,此例中我自定義的是CustomTextField,如下圖:

控制器注冊通知
// 注冊通知
[[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,相當(dāng)于把section也傳遞給contentTextField
cell.contentTextField.indexPath = indexPath;
return cell;
}
監(jiān)聽到通知后調(diào)用的方法
// 在這個方法中,我們就可以通過自定義textField的indexPath屬性區(qū)分不同行的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的重用,當(dāng)在willDisplayCell方法中重新配置cell時候,有if,就必須有else。因為之前屏幕上出現(xiàn)的cell離開屏幕被緩存起來時候,cell上的內(nèi)容并沒有清空,當(dāng)cell被重用時,系統(tǒng)并不會給我們把cell上之前配置的內(nèi)容清空掉,所以我們在else中對contentTextField內(nèi)容進行重新配置或者清空(根據(jù)自己的業(yè)務(wù)場景而定)
以下是方法二的demo地址
方法三(發(fā)送自定義通知)
其實方法三和方法二很像,都需要給自定義的textField添加indexPath屬性,也需要發(fā)送通知,然后在通知中心對這個通知注冊監(jiān)聽。區(qū)別在于,方法二發(fā)送的是系統(tǒng)自帶的通知UITextFieldTextDidEndEditingNotification,而方法三將要發(fā)送自定義通知。
1>給CustomTextField添加indexPath屬性。
2>給自定義cell添加CustomTextField類型contentTextField屬性。
3>cell遵守UITextFieldDelegate協(xié)議,成為textField屬性的delegate。
4>cell實現(xiàn)協(xié)議方法-textFieldDidEndEditing:(UITextField *)textField
5>textFieldDidEndEditing:協(xié)議方法中發(fā)送一個自定義的通知,并且把textField.text通過userInfo字典發(fā)出去。
具體實現(xiàn)代碼:
給CustomTextField添加indexPath屬性
#import <UIKit/UIKit.h>
@interface CustomTextField : UITextField
/**
* indexPath屬性用于區(qū)分不同的cell
*/
@property (strong, nonatomic) NSIndexPath *indexPath;
@end
給自定義cell添加CustomTextField類型contentTextField屬性
#import <UIKit/UIKit.h>
@class CustomTextField;
@interface TableViewCell : UITableViewCell
/**
* cell的標(biāo)題
*/
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
/**
* cell的文本框
*/
@property (weak, nonatomic) IBOutlet CustomTextField *contentTextField;
@end
遵守協(xié)議,設(shè)置delegate,實現(xiàn)協(xié)議方法
#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聚焦變成第一響應(yīng)者
[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>控制器注冊并監(jiān)聽該通知
7>在監(jiān)聽到通知的方法中通過userInfo拿到textField的text屬性
8>- (void)viewWillDisappear:(BOOL)animated方法中移除監(jiān)聽
9>完畢
注冊通知
// 如果不能保證控制器的dealloc方法肯定會被調(diào)用,不要在viewDidLoad方法中注冊通知。
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// 注意:此處監(jiān)聽的通知是:UITextFieldTextDidEndEditingNotification,textField結(jié)束編輯發(fā)送的通知,textField結(jié)束編輯時才會發(fā)送這個通知。
// 想實時監(jiān)聽textField的內(nèi)容的變化,你也可以注冊這個通知:UITextFieldTextDidChangeNotification,textField值改變就會發(fā)送的通知。
[[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];
// 在這個方法里移除通知,因為:
// 防止控制器被強引用導(dǎo)致-dealloc方法沒有調(diào)用
// 其他界面也有textField,其他界面的textField也會發(fā)送同樣的通知,導(dǎo)致頻繁的調(diào)用監(jiān)聽到通知的方法,而這些通知是這個界面不需要的,所以在視圖將要消失的時候移除通知 同樣,在視圖將要顯示的時候注冊通知
[[NSNotificationCenter defaultCenter] removeObserver:self name:@"CustomTextFieldDidEndEditingNotification" object:nil];
}
接收到通知回調(diào)方法
// 接收到注冊監(jiān)聽的通知后調(diào)用
- (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,可以使用二維數(shù)組,此處不再贅述
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的重用,當(dāng)在willDisplayCell方法中重新配置cell時候,有if,就必須有else。因為之前屏幕上出現(xiàn)的cell離開屏幕被緩存起來時候,cell上的內(nèi)容并沒有清空,當(dāng)cell被重用時,系統(tǒng)并不會給我們把cell上之前配置的內(nèi)容清空掉,所以我們在else中對contentTextField內(nèi)容進行重新配置或者清空(根據(jù)自己的業(yè)務(wù)場景而定)
以下是方法三的demo地址
方法三相對于方法二的好處在于:方法三發(fā)送的是自定義通知,而方法二發(fā)送的是系統(tǒng)自帶的通知。
因為項目開發(fā)中,受項目復(fù)雜度影響,難免會出現(xiàn)不同的控制器界面都會有UITextField類型(或者其子類型)的對象而沒有釋放,當(dāng)textField開始編輯、內(nèi)容發(fā)生改變、結(jié)束編輯時,都會發(fā)送相同的通知。此時如果我們采用監(jiān)聽系統(tǒng)自帶的通知的方法,就有可能監(jiān)聽到我們不需要的改變從而影響了業(yè)務(wù)數(shù)據(jù)。
舉個例子:A和B控制器都是UITableViewController類型的對象,A、B控制器界面上都有UITextField類型(或者其子類型)的子控件。并且A、B控制器都注冊了系統(tǒng)自帶的UITextField的通知UITextFieldTextDidChangeNotification,且監(jiān)聽到通知后都會調(diào)用各自的contentTextFieldTextDidChange:方法。當(dāng)A控制器pushB控制器后,我們在B控制器界面上的TextField編輯內(nèi)容,A控制器此時也監(jiān)聽了該通知,所以,A控制器上的contentTextFieldTextDidChange:方法也會被調(diào)用。這是我們不想得到的,所以,采用自定義通知的方法可以避免這一問題。
當(dāng)然,我們也可以在viewWillAppear:方法中注冊通知,然后在viewWillDisAppear:方法中移除通知,這樣同樣可以避免這一為題。
另外,值得提醒的是,如果我們不能保證控制器被pop時肯定會調(diào)用dealloc方法,那么建議在控制器的viewWillDisAppear:方法中移除通知,而非dealloc方法中移除。否則,用戶反復(fù)push、pop控制器時,控制器可能會注冊多份相同的通知。
方法四(使用block)
1>給cell添加一個block屬性,該block屬性帶有一個NSString *類型的參數(shù)。
2>給cell的textField添加target,觸發(fā)方法的事件是UIControlEventEditingChanged
3>textField觸發(fā)的方法中調(diào)用cell的這個block屬性,并把contentTextField.text作為block的參數(shù)傳進去
4>數(shù)據(jù)源方法cellForRowAtIndexPath:中對cell的block屬性賦值(也就是拿到cell.contentTextField.text)
5>數(shù)據(jù)源方法willDisplayCell:中對cell重新配置。
給cell添加一個block屬性
#import <UIKit/UIKit.h>
@interface TableViewCell : UITableViewCell
/**
* block 參數(shù)為textField.text
*/
@property (copy, nonatomic) void(^block)(NSString *);
/**
* cell的標(biāo)題
*/
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
/**
* cell的文本框
*/
@property (weak, nonatomic) IBOutlet UITextField *contentTextField;
@end
給textField addTarget
在事件觸發(fā)方法中調(diào)用block并傳遞參數(shù)
#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) {
// 更新數(shù)據(jù)源
[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。因為之前屏幕上出現(xiàn)的cell離開屏幕被緩存起來時候,cell上的內(nèi)容并沒有清空,當(dāng)cell被重用時,系統(tǒng)并不會給我們把cell上之前配置的內(nèi)容清空掉,所以我們在else中對contentTextField內(nèi)容進行重新配置或者清空(根據(jù)自己的業(yè)務(wù)場景而定)
customCell.contentTextField.text = [NSString stringWithFormat:@"第%ld組,第%ld行",indexPath.section,indexPath.row];
}
}
切記:對于cell的重用,當(dāng)在willDisplayCell方法中重新配置cell時候,有if,就必須有else。因為之前屏幕上出現(xiàn)的cell離開屏幕被緩存起來時候,cell上的內(nèi)容并沒有清空,當(dāng)cell被重用時,系統(tǒng)并不會給我們把cell上之前配置的內(nèi)容清空掉,所以我們在else中對contentTextField內(nèi)容進行重新配置或者清空(根據(jù)自己的業(yè)務(wù)場景而定)
以下是方法四的demo地址
方法四相對于方法二和方法三的好處在于:方法四沒有采用通知的方式來獲取contentTextField.text,而是采用靈活的block。并且方法四也無需自定義textField。
方法五(使用delegate實現(xiàn))
方法五和方法四很像,只不過方法五采用了delegate方式,更好的做到了解耦。
0>和方法二、方法三一樣,cell的textField屬性都需要使用自定義類型,因為我們需要給textField綁定indexPath屬性。
1>給cell制定一份協(xié)議,協(xié)議中有一個方法,帶有兩個參數(shù),一個是textField的text,另一個是indexPath。同時給cell添加一個delegate屬性。
2>給cell的textField添加target,觸發(fā)方法的事件是UIControlEventEditingChanged
3>textField觸發(fā)的方法中調(diào)用cell的協(xié)議方法,并把contentTextField.indexPath作為協(xié)議方法的參數(shù)傳進去
4>數(shù)據(jù)源方法cellForRowAtIndexPath:中對cell的indexPath賦值為當(dāng)前的indexPath。對cell的delegate賦值為當(dāng)前controller
5>控制器實現(xiàn)cell的協(xié)議方法,在協(xié)議方法里可以拿到textField的文本。
6>在tableView:willDisplayCell:forRowAtIndexPath:方法內(nèi)刷新tableView。
#import <UIKit/UIKit.h>
@class CustomTextField;
@protocol CustomCellCellDelegate <NSObject>
@required
// cell 的contentTextField的文本發(fā)生改變時調(diào)用
- (void)contentDidChanged:(NSString *)text forIndexPath:(NSIndexPath *)indexPath;
@end
@interface TableViewCell : UITableViewCell
/**
* cell的標(biāo)題
*/
@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 {
// 調(diào)用代理方法,告訴代理,哪一行的文本發(fā)生了改變
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];
}
以下是方法五的demo地址
文/VV木公子(簡書作者)
PS:如非特別說明,所有文章均為原創(chuàng)作品,著作權(quán)歸作者所有,轉(zhuǎn)載轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),并注明出處,所有打賞均歸本人所有!
如果您是iOS開發(fā)者,請關(guān)注本人,或者對本篇文章感興趣,請點擊喜歡,后續(xù)會更新更多相關(guān)文章!敬請期待!
如果有技術(shù)問題,歡迎加入QQ群進行交流,群聊號碼:194236752。