iOS, AutoLayoutで簡単にできるアニメーション

本記事は[KIT AppDeveloper Advent Calendar 2015](http://qiita.com/advent-calendar/2015/kitappdeveloper)の18日目の記事です。

# みなさん、アニメーションを利用していますか？

画面を引っ張った時のアニメーションや何かをタップして拡大するなど、意識せずに自然に利用しているアニメーションっていっぱいありますよね。
ユーザのアクションに対するフィードバックを与えるためにはアニメーションは重要なことだと思います。

[iOSヒューマンインターフェースガイドライン](https://developer.apple.com/jp/documentation/UserExperience/Conceptual/MobileHIG/Animation/Animation.html)にもこう書いてあります。

引用元：iOSヒューマンインターフェースガイドライン - アニメーション
> iOSのUIには気の利いた美しいアニメーションが活用されており、使っていて楽しいという印象を与えるのに役立っています。適切なアニメーションには次のような効果があります。
> ・状態を伝え、フィードバックを返す
> ・直接操作の感覚を高める
> ・ユーザのアクションの結果を視覚化する支援


# 何故AutoLayoutを利用した方が良いのか

Viewをアニメーションさせる際に端末のバージョンや画面回転などを考慮して
直接Viewの座標やサイズを変更すると、とても管理しやすいとは言えない状況になってくると思います。
また、問題が発生した時にどのアニメーションが悪さをしているのかも分かり辛いです。

そこで、**AutoLayout**の登場です。
**AutoLayoutの制約の値を変更するだけ**で、簡単にアニメーションを行うことができます。
Viewの座標やサイズではなく、制約の値を変更してアニメーションさせるため安全で管理しやすくなります。


# サンプルコード with デモ

### デモ

![animation.gif](https://qiita-image-store.s3.amazonaws.com/0/42327/452b1400-75ef-3608-358e-a8fb60c8dd47.gif)

### StoryBoard

四方全体に伸ばすようにAutoLayoutを設定しています。
<img width="592" alt="スクリーンショット 2015-12-17 17.45.16_min.png" src="https://qiita-image-store.s3.amazonaws.com/0/42327/a7828b98-32e5-5e0f-acbb-bcc372cebda3.png">

### Code

StoryBoardで配置したViewと設定したAutoLayoutをViewControllerへ紐付けています。
それぞれのボタンを押した時に**AutoLayoutの制約(constant)を変更してアニメーションする**ようにしています。

```ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var mainView: UIView!
    @IBOutlet weak var topConstraint: NSLayoutConstraint!
    @IBOutlet weak var leadingConstraint: NSLayoutConstraint!
    @IBOutlet weak var traningConstraint: NSLayoutConstraint!
    @IBOutlet weak var bottomConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    @IBAction func actionSmallButton(sender: AnyObject) {

        mainView.layoutIfNeeded()

        topConstraint.constant = view.bounds.size.height/4
        leadingConstraint.constant = view.bounds.size.width/4
        traningConstraint.constant = view.bounds.size.width/4
        bottomConstraint.constant = view.bounds.size.height/4

        UIView.animateWithDuration(0.2, animations: {
            self.mainView.layoutIfNeeded()
        })
    }

    @IBAction func actionLargeButton(sender: AnyObject) {
        mainView.layoutIfNeeded()

        topConstraint.constant = -64
        leadingConstraint.constant = -20
        traningConstraint.constant = -20
        bottomConstraint.constant = 0

        UIView.animateWithDuration(0.2, animations: {
            self.mainView.layoutIfNeeded()
        })
    }

    @IBAction func actionBounceButton(sender: AnyObject) {
        mainView.layoutIfNeeded()

        topConstraint.constant = view.bounds.size.height/4
        leadingConstraint.constant = view.bounds.size.width/4
        traningConstraint.constant = view.bounds.size.width/4
        bottomConstraint.constant = view.bounds.size.height/4

        UIView.animateWithDuration(0.2, animations: {
            self.mainView.layoutIfNeeded()
        }, completion: { (_) in

            self.mainView.layoutIfNeeded()

            self.topConstraint.constant = -64
            self.leadingConstraint.constant = -20
            self.traningConstraint.constant = -20
            self.bottomConstraint.constant = 0

            UIView.animateWithDuration(0.2, animations: {
                self.mainView.layoutIfNeeded()
            }, completion: nil)
        })

    }
    
}
```

# コード解説

先ほどのViewController.swiftの一部を抜粋しています。

```.swift
@IBAction func actionSmallButton(sender: AnyObject) {

    // 制約を変更する前に未適用時の表示を更新しておく
    mainView.layoutIfNeeded()

    // アニメーションさせたい値に制約を変更する
    // ここでは画面の1/4になるように変更しています
    topConstraint.constant = view.bounds.size.height/4
    leadingConstraint.constant = view.bounds.size.width/4
    traningConstraint.constant = view.bounds.size.width/4
    bottomConstraint.constant = view.bounds.size.height/4

    // 0.2秒間でアニメーションさせる
    UIView.animateWithDuration(0.2, animations: {
        // 制約の変更を適用させアニメーション実行する
        self.mainView.layoutIfNeeded()
    })
}
```

今回はサンプルのため一つのViewしか配置しませんでしたが
**複数のViewにAutoLayoutの制約が設定されている場合は、影響のあるView全てで`layoutIfNeeded()`を呼ぶ必要があります。**

# 小技

今回サンプルコードでは分かりやすくするため、制約を全て紐付けていましたが
Viewが増えてくると、制約を全てViewControllerに紐付けて管理することが難しくなってくると思います。

そのため、StoryBoardで設定した制約の**identifierをKeyとして、Viewから指定した制約を取得**できるメソッドを書いてみました。

```.swift
private func getConstraintFromView(view view: UIView, identifier: String) -> NSLayoutConstraint? {
    if let superView = view.superview {
        for constraint in superView.constraints {
            if let id = constraint.identifier where id == identifier {
                return constraint
            }
        }
    }
    return nil
}
```

解説で抜粋したコードに適応すると下記の様になります。

```.swift
@IBAction func actionSmallButton(sender: AnyObject) {

    mainView.layoutIfNeeded()

    if let top = getConstraintFromView(view: mainView, identifier: "mainTop") {
        top.constant = view.bounds.size.height/4
    }
    if let leading = getConstraintFromView(view: mainView, identifier: "mainLeading") {
        leading.constant = view.bounds.size.width/4
    }
    if let traning = getConstraintFromView(view: mainView, identifier: "mainTraning") {
        traning.constant = view.bounds.size.width/4
    }
    if let bottom = getConstraintFromView(view: mainView, identifier: "mainBottom") {
        bottom.constant = view.bounds.size.height/4
    }

    UIView.animateWithDuration(0.2, animations: {
        self.mainView.layoutIfNeeded()
    })
}
```