キーボードに追従するViewをAutolayoutで実装してみた



はじめに
----
   キーボード非表示時には画面下に配置され、キーボードが表示された時には「キーボードの移動に追従して」移動するViewを作ってみました。
   キーボードが表示/非表示された事をNSNotificationCenterで拾って、その後UIView.animationWithDuration()にてアニメーションしながらViewが移動します。このアニメーションにはAutolayoutの制約を使います。
   ![anime_viewWithKeyboard.gif](https://qiita-image-store.s3.amazonaws.com/0/7946/aace977e-d67c-b5b4-cd47-f0dc70d3d42c.gif "anime_viewWithKeyboard.gif")
[サンプルコード](https://github.com/taktamur/followKeyboardView)

Storyboardの構成
---
Storyboard上の構成では、「アニメーションで移動するViewの底」と「ViewControllerの底(Bottom Layout Guide)」の間に、高さの制約を作ります。またこれをIBOutletとしてViewControllerに接続しておきます。
この高さ制約の値は0としておきます。キーボードが表示された時には、この値をアニメーションで変化させる事で、キーボードに追従してViewを動かします。
![Main_storyboard_—_Edited.png](https://qiita-image-store.s3.amazonaws.com/0/7946/b92139d5-3c14-f951-fe5e-2c4a881a8d2b.png "Main_storyboard_—_Edited.png")


キーボードが表示される、隠れる際のNotificationの監視
---
キーボードが表示される時にはUIKeyboardWillShowNotificationが、非表示になる時にはUIKeyboardWillHideNotificationが飛びます。なのでこれを監視(addObserver)します。
この監視は、ViewControllerが表示されている間のみ必要なので、viewDidAppear〜viewWillDisappearの間のみ監視します。

Autolayoutの制約を使ったアニメーション
---
Autolayoutの制約でアニメーションを行うには、まず「制約の値を書き換えてから」、UIView.animateWithDuration()の中でlayoutIfNeeded()を使います。
参考：[Qiita:AutoLayout制約値を変更したアニメーションで勘違いしていた件](http://qiita.com/caesar_cat/items/051cda589afe45255d96)





タブバーの高さについて
---
タブバーが表示されている場合、Viewの移動量が変わります。

* タブバー非表示の場合：Viewの移動量=キーボードの高さ
* タブバー表示の場合：Viewの移動量=キーボードの高さ-タブバーの高さ

タブバーの高さも考慮しないと、キーボードとViewの間に隙間が出来てしまいます。
このタブバーの高さですが、UIViewControllerのbottomLayoutGuideで取得できます。タブバーが非表示の場合にはここが0になるので、if文での分岐等は不要です。

ソース
---
```swift:ViewController
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var bottomConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        self.startObserveKeyboardNotification()
    }

    override func viewWillDisappear(animated: Bool) {
        super.viewWillDisappear(animated)
        self.stopOberveKeyboardNotification()
    }

    /** キーボードを閉じるIBAction */
    @IBAction func tapView(sender: AnyObject) {
        self.textView.resignFirstResponder()
    }
}

/** キーボード追従に関連する処理をまとめたextenstion */
extension ViewController{
    /** キーボードのNotificationを購読開始 */
    func startObserveKeyboardNotification(){
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.addObserver(self, selector:"willShowKeyboard:", name: UIKeyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector:"willHideKeyboard:", name: UIKeyboardWillHideNotification, object: nil)
    }
    /** キーボードのNotificationの購読停止 */
    func stopOberveKeyboardNotification(){
        let notificationCenter = NSNotificationCenter.defaultCenter()
        notificationCenter.removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
        notificationCenter.removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
    }
    
    /** キーボードが開いたときに呼び出されるメソッド */
    func willShowKeyboard(notification:NSNotification){
        NSLog("willShowKeyboard called.")
        let duration = notification.duration()
        let rect     = notification.rect()
        if let duration=duration,rect=rect {
            // ここで「self.bottomLayoutGuide.length」を使っている理由：
            // tabBarの表示/非表示に応じて制約の高さを変えないと、
            // viewとキーボードの間にtabBar分の隙間が空いてしまうため、
            // ここでtabBar分の高さを計算に含めています。
            // - tabBarが表示されていない場合、self.bottomLayoutGuideは0となる
            // - tabBarが表示されている場合、self.bottomLayoutGuideにはtabBarの高さが入る
            
            // layoutIfNeeded()→制約を更新→UIView.animationWithDuration()の中でlayoutIfNeeded() の流れは
            // 以下を参考にしました。
            // http://qiita.com/caesar_cat/items/051cda589afe45255d96
            self.view.layoutIfNeeded()
            self.bottomConstraint.constant=rect.size.height - self.bottomLayoutGuide.length;
            UIView.animateWithDuration(duration, animations: { () -> Void in
                self.view.layoutIfNeeded()  // ここ、updateConstraint()でも良いのかと思ったけど動かなかった。
            })
        }
    }
    /** キーボードが閉じたときに呼び出されるメソッド */
    func willHideKeyboard(notification:NSNotification){
        NSLog("willHideKeyboard called.")
        let duration = notification.duration()
        if let duration=duration {
            self.view.layoutIfNeeded()
            self.bottomConstraint.constant=0
            UIView.animateWithDuration(duration,animations: { () -> Void in
                self.view.layoutIfNeeded()
            })
        }
    }
}
/** キーボード表示通知の便利拡張 */
extension NSNotification{
    /** 通知から「キーボードの開く時間」を取得 */
    func duration()->NSTimeInterval?{
        let duration:NSTimeInterval? = self.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? Double
        return duration;
    }
    /** 通知から「表示されるキーボードの表示位置」を取得 */
    func rect()->CGRect?{
        let rowRect:NSValue? = self.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue
        let rect:CGRect? = rowRect?.CGRectValue()
        return rect
    }
    
}
```

UITableViewControllerでは、この方法が使えなかった
---
UITableViewControllerは、tableViewの外側にViewを配置する事が出来ないので、ここで書いたやり方では対応出来ませんでした。StoryboardでViewを配置しようとすると「tableHeader/Footer」か「Cell」に割り振られてしまいます。
この場合、UIViewControllerの中にUITableViewを貼り付ける事で対応出来ました。