天天看點

iOS學習——tableview中帶編輯功能的cell鍵盤彈出遮擋和收起問題解決一 主要制器為UITableViewController或其子類二 主要制器為UIViewController或其子類

  最近在項目中經常用到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,顯示不出來或者顯示的位置不對。

iOS學習——tableview中帶編輯功能的cell鍵盤彈出遮擋和收起問題解決一 主要制器為UITableViewController或其子類二 主要制器為UIViewController或其子類

原文: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在整個編輯過程中的詳細流程步驟:

  1. 在成為第一響應者之前,文本框調用其代理的textFieldShouldBeginEditing:  方法來允許或阻止其第一響應者,并控制是否對文本框進行輸入
  2. 成為第一響應者,對應的相應事件就是系統調用鍵盤(自動彈出),并且系統會根據需要發出

    UIKeyboardWillShowNotification

    UIKeyboardDidShowNotification

    的Notification通知,而如果此時系統中有其他的輸入視圖是可視的,則系統會發出 UIKeyboardWillChangeFrameNotification和UIKeyboardDidChangeFrameNotification的通知

  3. 系統調用代理的 textFieldDidBeginEditing:  方法,并且發出

    UITextFieldTextDidBeginEditingNotification的通知,此時光标已經在text field中定位了,鍵盤也已經彈出來了,接下來可以進行輸入了

  4. 在輸入資訊過程中,目前文本内容改變就會調用,textField:shouldChangeCharactersInRange:replacementString: 方法,并且會發出

    UITextFieldTextDidChangeNotification

    的通知。此外,當使用者點選【clear/清除】按鍵時調用 textFieldShouldClear: 方法清除内容,當使用者點選【return/完成】按鍵時調用 textFieldShouldReturn: 方法,注意:UITextViewDelegate沒有對應清除和完成方法,是以我們不能調用textFieldShouldClear: 方法和 textFieldShouldReturn: 方法實作【clear/清除】和【return/完成】按鍵的效果 
  5. 在文本框輸入即将結束,即即将登出第一響應者時,系統會調用 textFieldShouldEndEditing: 方法
  6. 文本框登出第一響應者,對應的響應時間就是系統收回鍵盤,并且在隐藏鍵盤時會發出 

    UIKeyboardWillHideNotification和UIKeyboardDidHideNotification的通知

  7. 最後,系統調用 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;
    }];
}           

複制