UITextFieldがキーボードに隠れるのを防ぐ


画面下部のテキストボックス(`UITextField`)をタップしてキーボードが出てくる際、viewをスクロールさせてあげないとテキストボックスがキーボードの裏で隠れてしまう。iOS開発を自分でやってみて初めて知った事実。

`UITextField`を載っけているViewをスクロールさせて、これを防ぐ。

## 準備
まずはViewをStoryboardで普通に作っていく。Xcode7.2で試しています

スクロールさせなきゃいけないので、`UIViewController`を置いた後、`UIScrollView` => `UIView`の順にViewを重ねていく。

AutoLayoutの設定は詳しく書かないが、上記2つのviewとも親viewの上下左右の端にピッタリくっつける。一番最後に置いた`UIView`は、`UIViewController`についていた`UIView`（親の親view）と高さ・幅を等しくする。この最後に置いた`UIView`の上に`UITextField`をセット。

↓ こんな感じのStoryboardになる。


![スクリーンショット 2015-12-17 23.49.38.png](https://qiita-image-store.s3.amazonaws.com/0/1795/2e2b7d47-f5a9-a73c-f438-ed6328aa6274.png "スクリーンショット 2015-12-17 23.49.38.png")
	
`UITextField`をセットした`UIView`のAutolayout設定は以下。

![スクリーンショット 2015-12-17 23.42.41.png](https://qiita-image-store.s3.amazonaws.com/0/1795/c7731cb4-1cc3-610b-d043-8e8b6c4890b4.png "スクリーンショット 2015-12-17 23.42.41.png")

---

基本的なviewが出来たのでシミュレータで実行。`UITextField`は隠れてしまい、何が入力されているか分からない。

![Simulator Screen Shot 2015.12.17 23.51.22.png](https://qiita-image-store.s3.amazonaws.com/0/1795/f46fe793-895c-6380-ee75-24d829f1a86c.png "Simulator Screen Shot 2015.12.17 23.51.22.png")

## キーボードで隠れないように
というわけで、これを防ぐ。

### キーボードの表示/非表示の通知
キーボードが表示される/隠れる直前に、`UIKeyboardWillShowNotification`, `UIKeyboardWillHideNotification`というNotificationが飛んでいるので、それをオブザーバでキャッチできるようにする。

`UIScrollView`と`UITextField`をOutletでコード側で利用できるようにしておく。
また、`UITextField`のデリゲートを設定し、returnキーを押したらキーボードを隠す。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController, UITextFieldDelegate {

    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var textField: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        textField.delegate = self
    }
    
    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        
        NSNotificationCenter.defaultCenter().addObserver(self,
            selector: "keyboardWillBeShown:",
            name: UIKeyboardWillShowNotification,
            object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self,
            selector: "keyboardWillBeHidden:",
            name: UIKeyboardWillHideNotification,
            object: nil)
    }
    
    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        
        NSNotificationCenter.defaultCenter().removeObserver(self,
            name: UIKeyboardWillShowNotification,
            object: nil)
        NSNotificationCenter.defaultCenter().removeObserver(self,
            name: UIKeyboardWillHideNotification,
            object: nil)
    }
    
    func keyboardWillBeShown(notification: NSNotification) {
    }
    
    func keyboardWillBeHidden(notification: NSNotification) {
    }
    
    // MARK: - UITextFieldDelegate
    
    func textFieldShouldReturn(textField: UITextField) -> Bool {
        textField.resignFirstResponder()
        return true
    }
    
}
```

キーボードの表示 <=> 非表示の切替が可能になり、そのイベントが起こる際に`keyboardWillBeShown:`, `keyboardWillBeHidden:`を実行するようになった。

### 画面のスクロール処理
続いて、本題の画面スクロール処理を書いていく。以下の様な方針。

* `UIKeyboardWillShowNotification`の`userInfo`にキーボードの位置情報が入っているので利用
* キーボードの位置と`UITextField`の位置から、スクロールサイズを計算しスクロール
* `UIKeyboardWillShowNotification`には、キーボード表示時のアニメーション時間も含まれるため、その時間で画面をスクロールさせる

```swift:ViewController.swift
func keyboardWillBeShown(notification: NSNotification) {
    if let userInfo = notification.userInfo {
        if let keyboardFrame = userInfo[UIKeyboardFrameEndUserInfoKey]?.CGRectValue, animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey]?.doubleValue {
            restoreScrollViewSize()
            
            let convertedKeyboardFrame = scrollView.convertRect(keyboardFrame, fromView: nil)
            let offsetY: CGFloat = CGRectGetMaxY(textField.frame) - CGRectGetMinY(convertedKeyboardFrame)
            if offsetY < 0 { return }
            updateScrollViewSize(offsetY, duration: animationDuration)
        }
    }
}

func keyboardWillBeHidden(notification: NSNotification) {
    restoreScrollViewSize()
}

func updateScrollViewSize(moveSize: CGFloat, duration: NSTimeInterval) {
    UIView.beginAnimations("ResizeForKeyboard", context: nil)
    UIView.setAnimationDuration(duration)
    
    let contentInsets = UIEdgeInsetsMake(0, 0, moveSize, 0)
    scrollView.contentInset = contentInsets
    scrollView.scrollIndicatorInsets = contentInsets
    scrollView.contentOffset = CGPointMake(0, moveSize)
    
    UIView.commitAnimations()
}

func restoreScrollViewSize() {
    scrollView.contentInset = UIEdgeInsetsZero
    scrollView.scrollIndicatorInsets = UIEdgeInsetsZero
}
```

上記の方針にしたがって何を行っているかというと、次の通り。

* `UIKeyboardWillShowNotification`には、以下の情報が含まれているので、それを取得
	* キーボードのframeの情報(`UIKeyboardFrameEndUserInfoKey`)
	* キーボードのアニメーション時間(`UIKeyboardAnimationDurationUserInfoKey`)
* キーボードのframeの情報は、デバイスのウインドウ全体の中での座標になっているので、`convertRect(_:fromView:)`で`scrollView`内での座標に変換
* `textField`の下端とキーボードの上端のy座標（変換後）を比較
	* 前者のほうが大きかったら、キーボードに隠れないということなので処理終了
	* 後者が大きければキーボードから隠れないように、両者の差分だけスクロール処理
* `updateScrollViewSize(_:duration:)`でスクロール処理を実行
	* `scrollView`の`contentInset`, `scrollIndicatorInsets`を調整し、先ほど計算した差分の大きさだけ下方向に拡大
	* `scrollView`の`contentOffset`を調整し、y方向に同様の大きさだけスクロール
	* これらの処理を、取得済のアニメーション時間で実行

これで、キーボードが表示されるときに`UIScrollView`が上方向にスクロールするようになった。（※`UITextField`が隠れる場合のみ）

キーボードが隠れるときにはこれの逆の操作を`restoreScrollViewSize:`で行い、`scrollView`の`contentInset`, `scrollIndicatorInsets`をデフォルトの値に戻す。

## 完成
以上で、キーボードが隠れなくなり、自分が何を入力してるのか分かるようになった:smile:

![scroll-sample.gif](https://qiita-image-store.s3.amazonaws.com/0/1795/d4226c49-d205-d970-dd72-e24bf136239b.gif "scroll-sample.gif")