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時候,我需要在控制器中拿到諸如“法人姓名”這一類的資訊:
四個方法告訴你如何在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,如下圖:
控制器註冊通知
// 註冊通知
[[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];
}