[Swift][OSS] iOS 9以上でAuto Layoutの制約が簡潔に書けるTinyConstraintsを試してみた

TinyConstraints

はじめに

最近、離乳食でハチミツを食べていた男の子が乳児ボツリヌス症で亡くなったニュースに心を痛めている加藤です。悲しすぎますね...
さて、今回は記事執筆時点でStarの数が1400を超えているTinyConstraintsというSwiftのライブラリが良さげだったので試してみました。

検証環境

本記事は以下の環境で検証を行っています。

  • macOS Sierra バージョン 10.12.4
  • Xcode Version 8.3 (8E162)
  • iPhone 7シミュレータ iOS 10.3(14E269)
  • Swift 3.1
  • CocoaPods バージョン 1.2.0

TinyConstraintsとは

TinyConstraints is the syntactic sugar that makes Auto Layout sweeter for human use.

README.mdより引用

AutoLayoutが使いやすくなるシンタックスシュガーとのことです。 MITライセンスで公開されています。

ソースコードを見たところ、NSLayoutAnchorを使った制約のラッパーという感じでした。 NSLayoutAnchorはiOS 9以上でないと使えないのでご注意ください。 podspecでも「ios.deployment_target」が9.0になっています。

NSLayoutAnchorについては以前紹介記事を書きましたので宜ければご一読ください。

サンプルアプリ

以下はExampleをシミュレーターで動かした結果です。

TinyConstraints_sample

こんなのが簡単にできるって素敵ですね。

導入

早速導入してみます。CocoaPodsとCarthageに対応していますが、今回はCocoaPodsでインストールします。 pod initでPodfileを作成し、以下のようにpod "TinyConstraints"を記載します。 後はpod installすればOKです。

target ターゲット名 do
  use_frameworks!
  pod "TinyConstraints"
end

記事執筆時点では最新バージョン2.0.0がインストールされました。

使ってみる

pod installできたので使ってみます。
TinyConstraintsを使うにはファイルに以下のimport文が必要です。

import TinyConstraints

SubViewをSuperViewと同じサイズにする

SubViewをSuperViewと同じサイズにする場合、いくつか方法はありますがNSLayoutAnchorを使う場合は以下のような書き方になると思います。

let subView = UIView()
subView.translatesAutoresizingMaskIntoConstraints = false
subView.backgroundColor = .green
view.addSubview(subView)

// TinyConstraintsを使わない場合は通常こんな感じで書く
NSLayoutConstraint.activate([
    subView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
    subView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
    subView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
    subView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0)
])

上記のハイライト部分の制約をTinyConstraintsを使って書くと以下のようになります。

// TinyConstraintsを使った場合の書き方
subView.edges(to: view)

1行で書けてしまいます!素敵!!

edges

また、上下左右に余白をつけることも可能です。 以下は上下左右に16ptの余白をつける場合の例です。

// 上下左右に16ptずつ余白を設ける場合
subView.edges(to: view, insets: EdgeInsets(top: 16, left: 16, bottom: -16, right: -16))

edges_with_insets

SubViewの中心をSuperViewと同じにする

Viewのサイズは決まっているので中心だけSuperViewと同じにしたい場合もありますよね。 通常、centerXAnchorやcenterYAnchorを使って書く場合は以下のようになります。

let subView = UIView()
subView.translatesAutoresizingMaskIntoConstraints = false
subView.backgroundColor = .green
view.addSubview(subView)

// SubViewの幅と高さはSuperViewの2分の1
NSLayoutConstraint.activate([
    subView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.5),
    subView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5)
])

// SubViewの中心とSuperViewの中心を合わせる(TinyConstraintsを使わない場合)
NSLayoutConstraint.activate([
    subView.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
    subView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0)
])

上記のハイライト部分の制約をTinyConstraintsを使って書くと以下のようになります。

// SubViewの中心とSuperViewの中心を合わせる(TinyConstraintsを使った場合)
subView.center(in: view)

center_in

これも1行で書けてしまいました。

また、offsetを指定すれば中心からずらすことも可能です。

// 縦横に50ptずつずらす
subView.center(in: view, offset: CGPoint(x: 50, y: 50))

center_in_with_offset

幅や高さの制約

widthメソッド、heightメソッドを使うことで幅や高さの制約を付けられます。

// 幅と高さを100pt固定にする
subView.width(100)
subView.height(100)

CGSizeを指定しても幅と高さの制約をつけられます。

// 幅と高さを100pt固定にする
subView.size(CGSize(width: 100, height: 100))

〜以上、〜以下の制約もつけられます。

// 幅を10pt以上、50pt以下にする
subView.width(min: 10, max: 50)

また、relationやpriority、isActiveを指定することも可能です。

// 幅が100pt以上の制約
subView.width(100, relation: .equalOrGreater, priority: .required, isActive: true)

isActiveをfalseにし変数に保持することで、制約生成時には制約を適用せず、任意のタイミングで制約を有効にすることが可能です。

// 制約生成時は制約を有効にしない
let widthConstrant = subView.width(100, relation: .equalOrGreater, priority: .required, isActive: false)

// 任意のタイミングで制約を有効にする
widthConstrant.isActive = true

制約を保持しておくことで、後から制約の値を変更することが可能です。 以下は、初期状態として幅と高さを0ptにする制約を生成・保持しておき、 ボタンをタップしたら保持しておいた制約の幅と高さを200ptに変更する例です。 iOS 10から使えるUIViewPropertyAnimatorを使ってアニメーションさせてみました。

// 制約を保持しておく変数
var widthConstraint: Constraint!
var heightConstraint: Constraint!

override func viewDidLoad() {
    super.viewDidLoad()

    let subView = UIView()
    subView.translatesAutoresizingMaskIntoConstraints = false
    subView.backgroundColor = .green
    view.addSubview(subView)

    // 初期状態は幅と高さを0ptにしておく
    widthConstraint = subView.width(0)
    heightConstraint = subView.height(0)

    // SubViewの中心とSuperViewの中心を合わせる
    subView.center(in: view)
}

@IBAction func didTapStartAnimationButton(_ sender: Any) {
    // 幅と高さを200ptにアニメーション付きで変更
    widthConstraint.constant = 200
    heightConstraint.constant = 200

    UIViewPropertyAnimator(duration: 1.0, dampingRatio: 0.4) {
        self.view.layoutIfNeeded()
    }.startAnimation()
}

animation

Stackで縦方向 or 横方向にViewを並べる

stackメソッドを使えば、縦方向 もしくは 横方向にViewを並べることができます。 以下は3つのViewを垂直方向に並べる例です。

let subView1 = UIView()
subView1.translatesAutoresizingMaskIntoConstraints = false
subView1.backgroundColor = .green

let subView2 = UIView()
subView2.translatesAutoresizingMaskIntoConstraints = false
subView2.backgroundColor = .blue

let subView3 = UIView()
subView3.translatesAutoresizingMaskIntoConstraints = false
subView3.backgroundColor = .yellow

let views = [subView1, subView2, subView3]
view.stack(views, axis: .vertical, width: 100, height: 100, spacing: 10)

stack

垂直方向に並べた場合、指定したwidthは無視され、SuperViewと同じ幅になります。 heightを100ptに指定していますが、配列で指定したViewの内、最後のViewの高さは指定したheightが無視されてbottomAnchor(下辺)がSuperViewと一致する高さになります。 逆に水平方向に並べた場合は指定したheightが無視され、SuperViewと同じ高さになります。 また、配列で指定したViewの内、最後のViewの幅は指定したwidthが無視されてrightAnchor(右辺)がSuperViewと一致する幅になります。

おわりに

iOS 9以上でAuto Layoutの制約が簡潔に書けるTinyConstraintsを試してみました。 iOS 9以上で使えるNSLayoutAnchorを使うと制約を簡潔に書けますが、TinyConstraintsを使うと更に短く書けます。
使ってみてはいかがでしょうか。

参考記事