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


この記事はYahoo! JAPAN 18 新卒 2 Advent Calendar 2018 18日目の記事です。
前回の記事は@jiazioさんのSRv6で8パズルを解いてみるでした。

1枚目はこちら Yahoo! JAPAN 18 新卒 Advent Calendar 2018

はじめに

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

作るもの

アニメーションさせるにもなるべくコードを書かずに以下の様なアニメーションを実現します。

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

constantを変更する

水平方向の制約については以下の様に付けています。

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

swift4.0

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はこうなります。

swift4.0

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つ付ける必要があります。
素直に入れると以下の様に怒られます。

基本的に制約を付けると、priority = 1000となっているので、移動後に有効にしたい制約のpriorityを下げて差を付けます。他の制約との兼ね合いを見ながら調整します。ここでは900にしてます。

1000より小さくすると破線になります。

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

swift4.0

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

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()
        }
    }
}

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

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

終わりに

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