文系プログラマの勉強ノート

スマホアプリ開発やデザインなどについて勉強したことをまとめています

【Xcode】キーボードで隠れないようにスクロール

キーボードで入力エリアが隠れてしまう場合に、キーボードに合わせてスクロールさせるサンプルです。
先日の「【Xcode】文字入力できるTableViewCellサンプル - 文系プログラマの勉強ノート」のソースに追加していきます。

キーボード表示・非表示時の通知登録

ViewController.swiftに以下を追加します。

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        registerNotification()
    }
    
    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        unregisterNotification()
    }

// MARK: - Keyboard
    // 通知登録処理
    func registerNotification() -> () {
        let center: NSNotificationCenter = NSNotificationCenter.defaultCenter()
        center.addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
        center.addObserver(self, selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
    }
    
    // 通知登録解除処理
    func unregisterNotification() -> () {
        let center: NSNotificationCenter = NSNotificationCenter.defaultCenter()
        center.removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
        center.removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
    }

スクロール処理を作成

キーボード表示・非表示前に呼ばれるスクロール処理をViewController.swiftに以下のように追加します。

    // Keyboard表示前処理
    func keyboardWillShow(notification: NSNotification) -> () {
        scrollTableCell(notification, showKeyboard: true)
    }
    
    // Keyboard非表示前処理
    func keyboardWillHide(notification: NSNotification) -> () {
        scrollTableCell(notification, showKeyboard: false)
    }
    
    // TableViewCellをKeyboardの上までスクロールする処理
    func scrollTableCell(notification: NSNotification, showKeyboard: Bool) -> () {
        if showKeyboard {
            // keyboardのサイズを取得
            var keyboardFrame: CGRect = CGRectZero
            if let userInfo = notification.userInfo {
                if let keyboard = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue {
                    keyboardFrame = keyboard.CGRectValue()
                }
            }
            
            // keyboardのサイズが変化した分ContentSizeを大きくする
            let diff: CGFloat = keyboardFrame.size.height - lastKeyboardFrame.size.height
            let newSize: CGSize = CGSizeMake(tableView.contentSize.width, tableView.contentSize.height + diff)
            tableView.contentSize = newSize
            lastKeyboardFrame = keyboardFrame

            // keyboardのtopを取得
            let keyboardTop: CGFloat = UIScreen.mainScreen().bounds.size.height - keyboardFrame.size.height;
            
            // 編集中セルのbottomを取得
            let cell: InputTextTableCell = tableView.cellForRowAtIndexPath(NSIndexPath(forRow: editingPath.row, inSection: editingPath.section)) as! InputTextTableCell
            let cellBottom: CGFloat
            cellBottom = cell.frame.origin.y - tableView.contentOffset.y + cell.frame.size.height;
            
            // 編集中セルのbottomがkeyboardのtopより下にある場合
            if keyboardTop < cellBottom {
                // 編集中セルをKeyboardの上へ移動させる
                let newOffset: CGPoint = CGPointMake(tableView.contentOffset.x, tableView.contentOffset.y + cellBottom - keyboardTop)
                tableView.setContentOffset(newOffset, animated: true)
            }
        } else {
            // 画面を下に戻す
            let newSize: CGSize = CGSizeMake(tableView.contentSize.width, tableView.contentSize.height - lastKeyboardFrame.size.height)
            tableView.contentSize = newSize
            tableView.scrollToRowAtIndexPath(editingPath, atScrollPosition: UITableViewScrollPosition.None, animated: true)
            lastKeyboardFrame = CGRectZero;
        }
    }

ポイントは、

  1. キーボードの高さ分ContentSizeを加算する
  2. 前回のキーボード表示時の高さからの差分をContentSizeに加算する
  3. セルの座標をTableView上の座標から画面左上からの座標に換算する

です。

1は、下の方の行をキーボードの上まで手動でスクロールできるようにするためです。

2は、キーボードの表示イベントが続けて来た場合に対処するためです。例えば日本語入力で変換予測のバーが表示されたときや、ある行を編集中に別の行の編集を開始した場合など、 表示→非表示→表示 ではなく 表示→表示 とイベントがくるので、単純にキーボードの高さを足すとまずいことになります。同じサイズのキーボードであれば加算の必要はないですし、前より高ければ高くなった分だけ足すようにします。

3はキーボードの座標と比較するためです。セルのy座標からtableView.contentOffset.yを引くことで、キーボードと同じく画面左上からの座標になります。

編集中の行を取得、変数追加

上記ソースでまだ定義していない変数を使っていましたので追加します。
まず編集中の行を取得するため、InputTextTableCell.swiftに編集開始を通知する処理を追加します。

protocol InputTextTableCellDelegate {
    // 追加
    func textFieldDidBeginEditing(cell: InputTextTableCell, value: NSString) -> ()
}

class InputTextTableCell: UITableViewCell, UITextFieldDelegate {

   (中略)

    internal func textFieldDidBeginEditing(textField: UITextField) {
        self.delegate.textFieldDidBeginEditing(self, value: textField.text!)
    }
}

通知を受け、編集中の行を変数に保存する処理をViewController.swiftに追加します。
lastKeyboardFrameは前回表示されたキーボードのサイズを入れておく変数です。

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, InputTextTableCellDelegate {

    var lastKeyboardFrame: CGRect = CGRectZero
    var editingPath: NSIndexPath!

   (中略)

    func textFieldDidBeginEditing(cell: InputTextTableCell, value: NSString) -> () {
        let path = tableView.indexPathForRowAtPoint(cell.convertPoint(cell.bounds.origin, toView: tableView))
        editingPath = path
    }
}

UIScrollViewやUICollectionViewも同じ要領でいけると思います。