內含動態高度文字區塊的 TableViewCell 新舊作法

舊作法(iOS7以前)

在 iOS7(含)之前,內含動態高度文字區塊(UITextView)的 UITableViewCell 的作法頗為複雜:

1. 使用 Auto Layout,將TextView四邊邊界設為0

2. 在 cellForRowAtIndexPath 裡回傳 cell

到這裡與一般程式並沒有什麼不同。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    OldDynamicCell *cell = [tableView dequeueReusableCellWithIdentifier:@"OldDynamicCell" forIndexPath:indexPath];
    [cell.txtView setFont:[UIFont systemFontOfSize:15]];
    [cell.txtView setText:longString];
    return cell;
}

3. 在heightForRowAtIndexPath裡,使用sizeThatFits來計算動態高度

為了這麼作,就必需要有一模一樣的元件來計算才會準確。因此,我們另外 alloc 了一個 Cell來作這件事:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    if(oldDynamicCellCache==nil)
        oldDynamicCellCache = [[OldDynamicCell alloc]init];
    oldDynamicCellCache.txtView= [[UITextView alloc]init];
    [oldDynamicCellCache.txtView setFont:[UIFont systemFontOfSize:15]];
    [oldDynamicCellCache.txtView setText:longString];

    CGSize estimateTextViewSize = [oldDynamicCellCache.txtView sizeThatFits:CGSizeMake(tableView.bounds.size.width-10, FLT_MAX)];
    return estimateTextViewSize.height;
}

到此程式碼即可正常運作,不過如果不希望TextView 裡的文字可以捲動的話,可以進行下個步驟。

4. 將該 TextView 的 Scrolling Enable 選項清除

因為既然我們的文字區塊已可以動態調整,而 TableView 本身也可以 scroll 了,沒必要再讓 TextView 也可以 scroll 。

iOS 8 以後的新作法

iOS 8 引進了動態 Cell 的概念,簡化了所需要的步驟。這實在是開發者的一大福因。一些小地方往往會多花了時間,整個加起來就會導致開發效率降低。反之,若每個步驟都可以節省開發與測試的時間,整個專案執行下來的速度就會加快不少。

新作法如下:

1. 使用 Auto Layout,將TextView四邊邊界設為0

2. 在 cellForRowAtIndexPath 裡回傳 cell

到這裡與一般程式並沒有什麼不同。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NewDynamicCell *cell = [tableView dequeueReusableCellWithIdentifier:@"NewDynamicCell" forIndexPath:indexPath];
    [cell.txtView setFont:[UIFont systemFontOfSize:15]];
    [cell.txtView setText:longString];
    
    return cell;
}

3. 在ViewDidLoad裡加上新的設定程式碼

    self.tableView.rowHeight = UITableViewAutomaticDimension;
    self.tableView.estimatedRowHeight = 68.0;

4. 將該 TextView 的 Scrolling Enable 選項清除

注意:這個在新程式碼裡是必要的,否則無法運作。

5. 第五步… 沒有了,就這樣即完成了!真方便!

UITableViewAutomaticDimension 的運用不只限於 TextView,包含了其它元件在同一個 Cell 裡亦可動態調整。不過前題是 Auto Layout 必需要設好。如果 Cell 沒有照你所想的方式展開,9成9 是 Auto Layout 那裡不對。

Auto Layout 還不熟的新開發者們,好好磨練一下設的技巧吧~
堅持不使用 Auto Layout 的 Hardcore 開發者們,不要再ㄍ一ㄥ了~

範例程式可以在 https://github.com/iOTEC/dynamic_cell_height.git 找到

輕鬆處理最下方 UIButton 被 Keyboard 遮住的問題

按鈕放最下方的設計問題

有時候 App 為了設計上的需求,並不採用官方的右上 Edit/Done 的作法,而是將確定的按鈕放在最下方,我相信大家用 Windows 久了,也習慣看到按鈕在最下方。這個作法乍看下沒什麼問題…

BottomButton

不過在實作上,卻會遇到按鍵昇起之後擋住了按鈕的尷尬情況。 = =”

方法一:使用 UITableViewController 自動處理

如果 UI 是使用 UITableViewController 的話,很幸運的它會自動處理這個問題。這裡有個小技巧,要將 TextView 與 Button 放在同一個 Cell 裡,若放在下一個 Cell 的話還是一樣會被擋住。

降下 keyboard 作法:在 button 的動作裡加上

[self.textView resignFirstResponder];
不適用情形:相對的這個頁面就變成可以捲動了,並不一定會符合設計的需求。

方法二: 使用 UITextField的UITextFieldDelegate Protocol

如果上方的文字輸入區塊只需一行的話,可以使用 UITextField 來處理,而藉由 keyboard 的 textFieldShouldReturn 來處理:

作法如下:
1. 讓 UIViewController 實作 UITextFieldDelegate Protocol

@interface TextFieldViewController ()

2. 將按鍵設為 Done

[self.textField setReturnKeyType:UIReturnKeyDone]; 

3. 將 textfield.delegate 設為自己

self.textField.delegate = self;

4. 實作 textFieldShouldReturn 來降下 keyboard

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [textField resignFirstResponder];
    return YES;
}
不適用情形:如果上方的文字輸入區塊是允許換行的,就需使用 UITextView, 而上述的作法就無法進行。

方法三: 使用UITextView與 Auto Layout

其實這裡有個很簡單的作法來處理:在 Keyboard昇起的同時,也將 Button 昇到 keyboard 上方即可。


作法如下:

1. 先設好 AutoLayout constraints
TextView 的離上下左右皆為 20
Button 離左右下方為 30
Button 高度=50

2. 將 Button 離下方的 constraint 拉出來設 IBOutlet

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomHeightConstraint;

3. 在 ViewDidLoad 裡增加 listen keyboard notification

    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillChangeFrameNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];

4. 在 keyboard 昇起與降下時,改變 constraint 的值,這樣就可以達成目的了

#pragma mark - keyboard handling
- (void)keyboardWillShow:(NSNotification *)notification {
    NSDictionary *info = [notification userInfo];
    NSValue *kbFrame = [info objectForKey:UIKeyboardFrameEndUserInfoKey];
    NSTimeInterval animationDuration = [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    CGRect keyboardFrame = [kbFrame CGRectValue];

    CGFloat height = keyboardFrame.size.height;

    self.bottomHeightConstraint.constant = height+20;

    [UIView animateWithDuration:animationDuration animations:^{
        [self.view layoutIfNeeded];
    }];
}

- (void)keyboardWillHide:(NSNotification *)notification {
    NSDictionary *info = [notification userInfo];
    NSTimeInterval animationDuration = [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

    self.bottomHeightConstraint.constant = 20;
    [UIView animateWithDuration:animationDuration animations:^{
        [self.view layoutIfNeeded];
    }];
}

範例 GitHub : https://github.com/orangedream/TextViewAndKeyboard.git