AutoLayoutでアニメーションさせる話


この記事は[Yahoo! JAPAN 18 新卒 2 Advent Calendar 2018](https://adventar.org/calendars/3419)　18日目の記事です。
前回の記事は@jiazioさんの[SRv6で8パズルを解いてみる](https://qiita.com/jiazio/items/15b41f1345089a1887af)でした。

１枚目はこちら [Yahoo! JAPAN 18 新卒 Advent Calendar 2018](https://adventar.org/calendars/3388)

## はじめに
最近、アプリにアニメーションとか入れてリッチにしたいなーと思ってるんですが、入社してデザイナーさんとも一緒に作業することも多く、アニメーションさせるにもAutoLayoutを使っている前提でのアニメーションなどが必要になる機会が増えてきたので、そのあたりをまとめてみたいと思います。

## 作るもの
アニメーションさせるにもなるべくコードを書かずに以下の様なアニメーションを実現します。
![autoLayout.gif](https://qiita-image-store.s3.amazonaws.com/0/83234/1c087a52-cf9e-ae2d-360a-10b68bc93530.gif)

緑のViewに合わせてグレーのViewも変化していますが、やってることは緑のViewの左側の幅を変更しているだけです。
変更の仕方にも今回は2パターン記述します。

## constantを変更する
水平方向の制約については以下の様に付けています。
<img width="407" alt="スクリーンショット 2018-12-17 8.01.37.png" src="https://qiita-image-store.s3.amazonaws.com/0/83234/bfe2602a-8ca8-b670-b1f6-696d9d72bc73.png">

緑のViewの左側の`constant`の変化をグレーのViewのwidthで吸収するイメージです。コードで変化させる`NSLayoutConstraint`だけ、関連付けして値を動かせる様にします。

### swift4.0

```.swift
class ViewController: UIViewController {

    @IBOutlet weak var leftConstraint: NSLayoutConstraint!
    var isMoved = false

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

    @IBAction func tappedButton(_ sender: UIButton) {
        leftConstraint.constant = isMoved ? 50 : 100
        isMoved = !isMoved
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
}

```

もし、constantの値をベタ書きするのが嫌な場合は`@IBInspectable`でstoryboard側に持たせましょう。この方がデザイナーさんに優しい。storyboardはこうなります。
<img width="257" alt="スクリーンショット 2018-12-17 9.00.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/83234/565d2abe-879d-d6a8-e335-0d08e9415d98.png">

### swift4.0

```.swift
class ViewController: UIViewController {
    
    @IBInspectable var defaultConstant: CGFloat = 0.0
    @IBInspectable var movedConstant: CGFloat = 0.0
    
    @IBOutlet weak var leftConstraint: NSLayoutConstraint!
    var isMoved = false
    
    override func viewDidLoad() {
        super.viewDidLoad()
        leftConstraint.constant = defaultConstant
    }
    
    @IBAction func tappedButton(_ sender: UIButton) {
        leftConstraint.constant = isMoved ? movedConstant : defaultConstant
        isMoved = !isMoved
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
}
```

## isActiveで制約を使い分ける
次に、2つめのやり方として `NSLayoutConstraint.isActive`を変更する方法があります。
先ほどと同じ動作をさせて見ましょう。

その際に緑のViewの左の幅の制約を2つ付ける必要があります。
素直に入れると以下の様に怒られます。
<img width="275" alt="スクリーンショット 2018-12-17 7.26.02.png" src="https://qiita-image-store.s3.amazonaws.com/0/83234/a1a02519-b35a-bd2f-0c31-860b8f64cc3a.png">

基本的に制約を付けると、`priority = 1000`となっているので、移動後に有効にしたい制約の`priority`を下げて差を付けます。他の制約との兼ね合いを見ながら調整します。ここでは900にしてます。
<img width="218" alt="スクリーンショット 2018-12-17 7.45.33.png" src="https://qiita-image-store.s3.amazonaws.com/0/83234/b293f044-55c2-6dde-584b-7aa6d078b4ad.png">

1000より小さくすると破線になります。
<img width="271" alt="スクリーンショット 2018-12-17 7.26.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/83234/32f02060-7ab9-bf18-5abf-19517a3d07e1.png">

この状態で、以下の様に実装すると同様の動きが得られます。

### swift4.0

```.swift
class ViewController: UIViewController {

    @IBOutlet var defaultLeftConstraint: NSLayoutConstraint!
    @IBOutlet var movedLeftConstraint: NSLayoutConstraint!
    
    var isMoved = false

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

    @IBAction func tappedButton(_ sender: UIButton) {
        defaultLeftConstraint.isActive = !isMoved
        movedLeftConstraint.isActive = isMoved
        isMoved = !isMoved
        
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
}
```

### swift4.2

```.swift
class ViewController: UIViewController {

    @IBOutlet var defaultLeftConstraint: NSLayoutConstraint!
    @IBOutlet var movedLeftConstraint: NSLayoutConstraint!
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func tappedButton(_ sender: UIButton) {
        defaultLeftConstraint.isActive.toggle()
        movedLeftConstraint.isActive.toggle()
        
        UIView.animate(withDuration: 0.3) {
            self.view.layoutIfNeeded()
        }
    }
}
```

注意する点として、 `NSLayoutConstraint`を`weak`にしていると、isActive = falseのときに制約自体がnilになってしまうので、このやり方の場合は強参照で持たせましょう。

constantを変更する方がシンプルで簡単ですが、cellなどの利用して複雑な構成になってくるとisActiveで制約を使い分ける方法も有効になって来ます。

## 終わりに
サクッと触りの部分だけ書きましたが、もっと色々追求して行こうと思ってます。こんなのもあるよーなどあれば教えてくださいー！
`追記` 4.2からtoggle()が良いよって話があったので、それ用にもコード足しました。