[iOS 9] NSLayoutAnchorを使ってNSLayoutConstraintを簡潔に書こう!

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

こんにちは!加藤潤です。
これまでコードでNSLayoutConstraintを生成する場合、

public class func constraintsWithVisualFormat(format: String, options opts: NSLayoutFormatOptions, metrics: [String : AnyObject]?, views: [String : AnyObject]) -> [NSLayoutConstraint]

    public convenience init(item view1: AnyObject, attribute attr1: NSLayoutAttribute, relatedBy relation: NSLayoutRelation, toItem view2: AnyObject?, attribute attr2: NSLayoutAttribute, multiplier: CGFloat, constant c: CGFloat)

を使っていたかと思います。 VisualFormatは独特の記法を覚える必要がありますし、convenience initの方は引数多いよ!と思っていましたが iOS 9からNSLayoutAnchorというクラスが追加され、より簡潔にNSLayoutConstraintを生成できるようになったようなので試してみました。

開発環境

開発環境は下記の通りです。

  • Xcode 7.1
  • iPhone 6sシミュレータ

やりたいこと

今回はViewControllerのルートビュー上にサブビューを1つ貼り付けて以下の制約を追加してサブビューを固定します。

  • サブビューのTop側をTop Layout GuideのBottomに合わせる
  • サブビューのBottom側をBottom Layout GuideのTopに合わせる
  • サブビューのLeading側をルートビューのLeading Marginに合わせる
  • サブビューのTrailing側をルートビューのTrailing Marginに合わせる

Storyboardでやる場合は以下のようになります。 ios-9-nslayoutanchor_001

これと同じことをコードでNSLayoutConstraintを生成してやってみます。

準備

まずはViewControllerのルートビューにサブビューを1つ貼り付けましょう。貼り付けた場所で構わないのでPinの制約で位置を固定します。 そして制約の「Remove at build time」にチェックを入れます。

ios-9-nslayoutanchor_002

Why 'Remove at build time' ?

なんでわざわざStoryboard上で制約を設定してビルド時に取り除くなんてやっているの? 最初からStoryboard上の制約設定しなければいいんじゃないの?どうせコードでNSLayoutConstraint生成するんだし。と思いますが、 Storyboard上で制約が設定されていないと、NSIBPrototypingLayoutConstraintという制約をInterface Builderがビルド時に自動生成します。 その制約とコード上の制約がバッティングすると実行時にUnable to simultaneously satisfy constraintsのエラーがコンソールに出力されます。 少々回りくどいことをやっているのはこのエラーを回避するためです。

コーディング(NSLayoutConstraintを直接生成)

まずは従来通り、NSLayoutConstraintを直接生成する方法でやってみます。 ViewControllerのupdateViewConstraintsをオーバーライドします。

    override func updateViewConstraints() {
        super.updateViewConstraints()
        
        // 上側
        NSLayoutConstraint(item: self.subView,
            attribute: .Top,
            relatedBy: .Equal,
            toItem: self.topLayoutGuide,
            attribute: .Bottom,
            multiplier: 1.0,
            constant: 0.0).active = true
        
        // 下側
        NSLayoutConstraint(item: self.subView,
            attribute: .Bottom,
            relatedBy: .Equal,
            toItem: self.view,
            attribute: .BottomMargin,
            multiplier: 1.0,
            constant: 0.0).active = true
        
        // 左側
        NSLayoutConstraint(item: self.subView,
            attribute: .Leading,
            relatedBy: .Equal,
            toItem: self.view,
            attribute: .LeadingMargin,
            multiplier: 1.0,
            constant: 0.0).active = true
        
        // 右側
        NSLayoutConstraint(item: self.subView,
            attribute: .Trailing,
            relatedBy: .Equal,
            toItem: self.view,
            attribute: .TrailingMargin,
            multiplier: 1.0,
            constant: 0.0).active = true
    }

実行をすると下記のようなレイアウトになります。 ios-9-nslayoutanchor_003

コーディング(NSLayoutAnchorを使用)

次にNSLayoutAnchorを使用してNSLayoutConstraintを生成する方法でやってみます。 同様にViewControllerのupdateViewConstraintsをオーバーライドします。

    override func updateViewConstraints() {
        super.updateViewConstraints()
        
        // マージンを取得
        let margins = self.view.layoutMarginsGuide
        
        // 上側
        self.subView.topAnchor.constraintEqualToAnchor(self.topLayoutGuide.bottomAnchor).active = true
        
        // 下側
        self.subView.bottomAnchor.constraintEqualToAnchor(margins.bottomAnchor).active = true
        
        // 左側
        self.subView.leadingAnchor.constraintEqualToAnchor(margins.leadingAnchor).active = true
        
        // 右側
        self.subView.trailingAnchor.constraintEqualToAnchor(margins.trailingAnchor).active = true
    }

解説すると下記のような感じです。

  1. self.view.layoutMarginsGuideでルートビューのマージンを取得します。(ここで返ってくる型はUILayoutGuideというこちらもiOS 9で追加されたクラスです。)
  2. サブビューのtopAnchor(NSLayoutYAxisAnchorというNSLayoutAnchorのサブクラス)のconstraintEqualToAnchorメソッドを呼んでアンカーを紐付けます。ここで引数はtopLayoutのbottomと紐付けたいのでself.topLayoutGuide.bottomAnchorです。そしてこのメソッドを呼ぶとNSLayoutConstraintが返ってくるのでactive=trueにします。
  3. 同様に下側、左側、右側の制約も設定します。(上側の制約と違い紐づける対象がルートビューのマージンになっています。)

まとめ

いかがでしたでしょうか。 NSLayoutAnchorを使うとNSLayoutConstraintを直接生成する場合に比べてだいぶ簡潔に書けました。 iOS 9ではレイアウトに関するAPIが色々と追加されています。 Autolayoutがだんだんと開発者に易しくなってきたように感じる今日この頃です。

参考