最近在項目中經常用到UITableView中的cell中帶有UITextField或UITextView的情況,然后在這種場景下,當我們點擊屏幕較下方的cell進行編輯時,這時候鍵盤彈出來會出現遮擋待輸入的cell,導致我們無法很方便地查看到我們輸入的內容,這樣的體驗是非常不好的。這個問題在之前我們的隨筆iOS學習——鍵盤彈出遮擋輸入框問題解決方案中也有講過對應的解決方案,但是該方案在最近的應用中還有點小問題,我們在這里重新進行處理好。
一 主控制器為UITableViewController或其子類
首先,有一個很簡單的解決方案,就是將我們的控制器換成UITableViewController或其子類,UITableViewController中的cell當有鍵盤彈出的時候表單整體會自動進行上移,我們需要編輯的區域正好可以在鍵盤的上方,這樣我們正好也可以看到我們編輯的內容,方便我們進行修改和調整具體內容。
但是,如果我們的整體布局並不是只有一個UITableView,或者我們在項目中需要用到MBProgressHUD框架時,我們可能就不能直接將我們的控制器設置成UITableViewController或其子類,因為MBProgressHUD框架在UITableViewController和UICollectionViewController中顯示會存在一些bug,在GitHub中的MBProgressHUD框架官方文檔中就有提到要避免將HUD添加到具有復雜視圖層次結構的某些UIKit視圖(如UITableView或UICollectionView),UITableViewController和UICollectionViewController中的self.view實際上就是對應的UITableView或UICollectionView,所以會出現一些莫名其妙的bug,顯示不出來或者顯示的位置不對。

原文:You can add the HUD on any view or window. It is however a good idea to avoid adding the HUD to certain UIKit views with complex view hierarchies - like UITableView or UICollectionView. Those can mutate their subviews in unexpected ways and thereby break HUD display.
翻譯:你可以在任何視圖或窗口上添加HUD。 然而,避免將HUD添加到具有復雜視圖層次結構的某些UIKit視圖(如UITableView或UICollectionView)是一個好主意。 這可能以意想不到的方式改變他們的subviews,從而破壞HUD顯示。
二 主控制器為UIViewController或其子類
其實最開始我就是用的UITableViewController,結果要提示的要提示的tips總是顯示不設定的位置上,后來才得以發現的這個bug,我也很無奈😂🤷♀️,我們的項目匯總因為用到了MBProgressHUD框架,所以只能是用UIViewController上布局一個UITableView來實現,這樣我們再self.view上布局MBProgressHUD時才避開了UITableView或UICollectionView,然后就都沒問題了。言歸正傳,下面就說回到我們要解決的問題,在UITableView的cell中,系統自帶的UITableViewCell的格式沒有自帶UITextField或UITextView這種可以編輯的區域的,而這種類型的cell在我們的項目開發包中經常要用到,所以我們就需要對這類cell進行封裝和自定義。
2.1 UITextField或UITextView點擊之后的詳細流程
在對cell進行封裝和自定義的時候,我們需要考慮我們的UITextField或UITextView從點擊編輯框到結束編輯的整個過程是怎么樣的,在這個過程中我們需要回傳什么信息,才能保證我們的可以對我們控制器中的tableview進行控制。下面的流程就是UITextField或UITextView在整個編輯過程中的詳細流程步驟:
- 在成為第一響應者之前,文本框調用其代理的textFieldShouldBeginEditing: 方法來允許或阻止其第一響應者,並控制是否對文本框進行輸入
- 成為第一響應者,對應的相應事件就是系統調用鍵盤(自動彈出),並且系統會根據需要發出
UIKeyboardWillShowNotification和UIKeyboardDidShowNotification的Notification通知,而如果此時系統中有其他的輸入視圖是可視的,則系統會發出UIKeyboardWillChangeFrameNotification和UIKeyboardDidChangeFrameNotification的通知 - 系統調用代理的 textFieldDidBeginEditing: 方法,並且發出
UITextFieldTextDidBeginEditingNotification的通知,此時光標已經在text field中定位了,鍵盤也已經彈出來了,接下來可以進行輸入了 - 在輸入信息過程中,當前文本內容改變就會調用,textField:shouldChangeCharactersInRange:replacementString: 方法,並且會發出
UITextFieldTextDidChangeNotification的通知。此外,當用戶點擊【clear/清除】按鍵時調用 textFieldShouldClear: 方法清除內容,當用戶點擊【return/完成】按鍵時調用 textFieldShouldReturn: 方法,注意:UITextViewDelegate沒有對應清除和完成方法,所以我們不能調用textFieldShouldClear: 方法和 textFieldShouldReturn: 方法實現【clear/清除】和【return/完成】按鍵的效果 - 在文本框輸入即將結束,即即將注銷第一響應者時,系統會調用 textFieldShouldEndEditing: 方法
- 文本框注銷第一響應者,對應的響應時間就是系統收回鍵盤,並且在隱藏鍵盤時會發出
UIKeyboardWillHideNotification和UIKeyboardDidHideNotification的通知 - 最后,系統調用 textFieldDidEndEditing: 方法結束輸入,並發出
UITextFieldTextDidEndEditingNotification的通知。
2.2 自定義包含UITextField的UITableViewCell
首先,我們在點擊編輯區域的時候,獲取到當前編輯區域相對屏幕的位置,這樣方便我們判斷整個tableview是否需要上移以及需要上移多少比較合適。當然,我們自定義的cell中的UITextField或UITextView的代理設為cell自己,具體實現如下:
#import <UIKit/UIKit.h> typedef void(^ContentEditResultBlock)(NSString *contentString); typedef void(^ContentStartEditBlock)(CGRect frameToView); @interface BasicCell : UITableViewCell @property (copy, nonatomic) NSString *title; //左側標題欄 @property (copy, nonatomic) NSString *placeHolder; //沒有內容時的提示 @property (copy, nonatomic) NSString *content; //內容 @property (assign, nonatomic) BOOL isForbidEdit; //是否允許編輯 @property (assign, nonatomic) BOOL isHiddenLine; //是否隱藏分割線 //編輯結束時的回調 @property (copy, nonatomic) ContentEditResultBlock contentEditResultBlock; //編輯開始時的回調 @property (copy, nonatomic) ContentStartEditBlock contentStartEditBlock; @end
在這里,我們定義了兩個回調block,分別在編輯區域開始編輯(textFieldDidBeginEditing: )和結束編輯(textFieldDidEndEditing: )的時候調用,開始編輯的時候返回當前cell相對屏幕的位置方便我們控制是否上移tableview,結束編輯時返回我們編輯框的內容方便進行記錄。具體實現的代碼如下:
1 #import "BasicCell.h" 2 @interface BasicCell () <UITextFieldDelegate> 3 @property (strong, nonatomic) UILabel *titleLabel; //標題欄 4 @property (strong, nonatomic) UITextField *contentField; //內容欄 5 @property (strong, nonatomic) UIView *lineView; //分割線 6 7 @end 8 9 @implementation CJMeetingReplyBasicCell 10 11 - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{ 12 self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 13 CGFloat fontSize = 16.0f; 14 if (IS_IPHONE_5 || IS_IPHONE_5S) { 15 fontSize = 15.0f; 16 } else { 17 fontSize = 16.0f; 18 } 19 //標題欄 配置 20 _titleLabel = [[UILabel alloc] init]; 21 _titleLabel.font = FONT(fontSize); 22 _titleLabel.textColor = kGrayFontColor; 23 //內容欄 配置 24 _contentField = [[UITextField alloc] init]; 25 _contentField.font = FONT(fontSize); 26 _contentField.textColor = kBlackFontColor; 27 _contentField.textAlignment = NSTextAlignmentRight; 28 _contentField.returnKeyType = UIReturnKeyDone; 29 _contentField.delegate = self; 30 //分割線 配置 31 _lineView = [[UIView alloc] init]; 32 _lineView.backgroundColor = kLineColor; 33 //添加到 cell中 34 [self addSubview:_titleLabel]; 35 [self addSubview:_contentField]; 36 [self addSubview:_lineView]; 37 //布局 38 WEAKSELF 39 CGFloat ratio = 230.0f/375.0f; 40 [_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { 41 make.top.bottom.mas_equalTo(weakSelf).mas_offset(0.0f); 42 make.left.mas_equalTo(weakSelf).mas_offset(15.0f); 43 make.right.mas_equalTo(weakSelf.mas_left).mas_offset((1-ratio-0.02)*ZYAppWidth); 44 }]; 45 [_contentField mas_makeConstraints:^(MASConstraintMaker *make) { 46 make.top.bottom.mas_equalTo(weakSelf).mas_offset(0.0f); 47 make.left.mas_equalTo(weakSelf.mas_right).mas_offset(-(ratio*ZYAppWidth)); 48 make.right.mas_equalTo(weakSelf).mas_offset(-15.0f); 49 }]; 50 [_lineView mas_makeConstraints:^(MASConstraintMaker *make) { 51 make.left.mas_equalTo(weakSelf).mas_offset(15.0f); 52 make.right.mas_equalTo(weakSelf).mas_offset(0.0f); 53 make.bottom.mas_equalTo(weakSelf).mas_offset(0.0f); 54 make.height.mas_equalTo(0.5f); 55 }]; 56 57 return self; 58 } 59 60 - (void)setTitle:(NSString *)title{ 61 self.titleLabel.text = title; 62 } 63 64 - (void)setContent:(NSString *)content{ 65 self.contentField.text = content; 66 } 67 68 - (void)setPlaceHolder:(NSString *)placeHolder{ 69 self.contentField.placeholder = placeHolder; 70 } 71 72 - (void)setIsForbidEdit:(BOOL)isForbidEdit{ 73 self.contentField.enabled = !isForbidEdit; 74 } 75 76 - (void)setIsHiddenLine:(BOOL)isHiddenLine{ 77 self.lineView.hidden = isHiddenLine; 78 } 79 80 #pragma mark -- UITextField代理 81 - (void)textFieldDidBeginEditing:(UITextField *)textField{ 82 CGRect frame = [textField convertRect:textField.frame toView:nil]; 83 if (_contentStartEditBlock) { 84 _contentStartEditBlock(frame); 85 } 86 } 87 88 - (void)textFieldDidEndEditing:(UITextField *)textField{ 89 NSString *contentString = textField.text; 90 if (_contentEditResultBlock) { 91 _contentEditResultBlock(contentString); 92 } 93 } 94 95 #pragma mark - textField delegate 96 - (BOOL)textFieldShouldReturn:(UITextField *)textField { 97 [textField resignFirstResponder]; 98 return YES; 99 } 100 101 @end
2.3 對自定義cell的應用
我們在對tableview的上移進行調整時,我們需要知道當前編輯的cell相對屏幕的位置,然后才能判斷是否需要上移tableview以及上移多少。所以我們在cell的編輯區域開始編輯(textFieldDidBeginEditing: ),需要回傳自身的位置,就是通過block將當前cell相對屏幕的frame回傳到我們的主控制器。
- (void)textFieldDidBeginEditing:(UITextField *)textField{ //獲取當前cell相對屏幕的位置 CGRect frame = [textField convertRect:textField.frame toView:nil]; if (_contentStartEditBlock) { _contentStartEditBlock(frame); } }
主控制器中對自定義cell的應用,首先,我們再主控制器中定義幾個屬性來保存我們鍵盤彈出時tableview的contentOffset以及當前編輯cell的frame,然后在應用自定義cell時設定我們的兩個回調block,當開始編輯時,通過回調block回傳的frame參數設置對應的editFrame。具體代碼如下:
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource> @property (strong, nonatomic) UITableView *tableView; @property (strong, nonatomic) NSMutableArray<NSMutableArray<CellInfoModel *> *> *cellInfoForParts; //保存當前編輯cell的frame @property (assign, nonatomic) CGRect editFrame; //保存鍵盤彈出前tableview的contentOffset,方便我們在鍵盤收起時將tableview進行還原程原先的位置 @property (assign, nonatomic) CGPoint lastContentOffset; @end
下面是應用自定義cell的代碼:
#pragma mark - Table view data source - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { //當前行的數據模型 CellInfoModel *cellModel = _cellInfoForParts[indexPath.section][indexPath.row]; BasicCell *cell = [tableView dequeueReusableCellWithIdentifier:@"BasicCell"]; if (!cell) { cell = [[BasicCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"BasicCell"]; } cell.title = cellModel.title; cell.content = [self getInfo:cellModel.propName]; cell.placeHolder = cellModel.placeHolder; cell.isForbidEdit = cellModel.isForbidEdit; cell.selectionStyle = cellModel.selectionStyle; WEAKSELF cell.contentEditResultBlock = ^(NSString *contentString) { //編輯完成后的處理,一般是數據保存 NSLog(@"result %@", contentString); }; cell.contentStartEditBlock = ^(CGRect frameToView) { weakSelf.editFrame = frameToView; }; return cell; }
2.4 鍵盤的彈出和收起
在前面的2.1的UITextField或UITextView點擊之后的詳細流程分析中我們知道,在點擊文本之后彈出鍵盤時會發送一個UIKeyboardWillShowNotification的通知,在編輯結束之后收起鍵盤時則也會發送一個UIKeyboardWillHideNotification的通知,所以我們通過監聽這兩個通知,來采取對應的行動。那么,首先我們需要對對應的通知進行注冊,然后設置在監聽到對應的通知之后應該采取的行動和措施。
注冊通知的方法:
#pragma mark notification 通知管理 /** * @brief 通知注冊 * @return */ - (void)registNotification { //彈出鍵盤的通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; //收起鍵盤的通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; }
彈出鍵盤對應的操作,如果有遮擋,我們通過修改tableview的contentOffset來實現tableview的上移:
#pragma mark --鍵盤彈出收起管理 -(void)keyboardWillShow:(NSNotification *)note{ CGRect frame = _editFrame; //保存鍵盤彈出前tableview的contentOffset偏移 self.lastContentOffset = self.tableView.contentOffset; //獲取鍵盤高度 NSDictionary *info = [note userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; //判斷鍵盤彈出是否會遮擋當前編輯cell,frame.size.height是當前編輯cell的高度 CGFloat offSet = frame.origin.y + frame.size.height - (self.view.frame.size.height - kbSize.height); //將試圖的Y坐標向上移動offset個單位,以使線面騰出開的地方用於軟鍵盤的顯示 if (offSet > 0.01) { WEAKSELF //有遮擋時,tableview需要的偏移量應該是在原先的基礎上再往上上移的,這里我們默認增加10個單位的空白 offSet += self.lastContentOffset.y + 10; [UIView animateWithDuration:0.1 animations:^{ weakSelf.tableView.contentOffset = CGPointMake(0, offSet); }]; } }
收起鍵盤的操作,和彈出鍵盤相對,彈出鍵盤時我們保存了彈出鍵盤之前tableview的contentOffset的偏移量,所以,在收起鍵盤后,我們將tableview的contentOffset值設為彈出之前的值就可以了,回到鍵盤彈出之前的狀態了。
-(void)keyboardWillHide:(NSNotification *)note{ WEAKSELF [UIView animateWithDuration:0.1 animations:^{ weakSelf.tableView.contentOffset = weakSelf.lastContentOffset; }]; }
