プログラマブログ

by wacul

menu
  • プログラマ
  • iOSアプリで Programmable な自動レイアウト(NSLayoutConstraint)

2014.05.07iOSアプリで Programmable な自動レイアウト(NSLayoutConstraint)

はじめまして、プログラマチームの kyoh です。 最近は津軽金山焼(PV:YouTube)に惚れてます。

背景

ワカルでは現在、RubyMotionを使ってiPadアプリを開発中です。 プロジェクトとして「InterfaceBuilderを使わない」という選択をした結果、より自由度の高い NSLayoutConstraint の魅力がわかってきました。 これはもったいない、世の中的にももっと使ってもらえるように、とこの記事を書くに至った次第です。

Objective-C はちょっと・・・というスタンスなので、ソースコードは全てRubyで書いています。すみません。

結論

InterfaceBuilderを使わない変態算数が好きなプログラマーは NSLayoutConstraint.constraintWithItem… をガシガシ使おう!CSS風に書ける快感を皆で味わおう! メソッド名が長い?記述が読みにくい?それならLayoutExpressでどうだっ!(自薦)

NSLayoutConstraint の機能

NSLayoutConstraint という名前の通り、ビューの位置やサイズを他のビューの位置やサイズに従って制約(constrain)することで、自動レイアウトする機能を提供しています。 例えば、親ビューの幅が変わったら子のビューの幅も従属する、というような場合も、いちいちユーザーのアクションに従ってframeを再設定する必要がありません

NSLayoutConstraint には、2つのクラスメソッドが用意されています。

  • constraintsWithVisualFormat:options:metrics:views: Visual Format Language (以下、VFL)を使って、可視的にレイアウトを記述できます。 比較的簡単、かつ他所でも記事が充実(例1,例2,例3)しているのでここではこちらの詳しい説明は割愛します。
  • constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant: めまいのする長さのメソッドですが、こちらが今回の記事の要です。

constraintWithItem…の使い方

引数が多い上に名前が長く、そして公式ドキュメントの記述が複雑であるがゆえに、障壁の高いこちら。 ですが、落ち着いて紐解いてみればさほど難しいことをやっているわけではありません。

1
NSLayoutConstraint.constraintWithItem(view1, attribute:attr1, relatedBy:relation, toItem:view2, attribute:attr2, multiplier:multiplier, constant:constant)

・・・うーん、長い。このメソッドで作ることができるのは、次のような制約です。

view1.attr1 [||] view2.attr2 × multiplier + constant

  • view1 の属性 attr1 を、view2 の属性 attr2 に従って制約する。
    • 逆ではない(view2 を制約するものではない)ので注意
    • サイズや位置を単なる数値で指定したい場合、view2 に nil を指定すればいい。
  • attr1, attr2 は、幅(width)、高さ(height)、位置(left / right / top / bottom / centerX / centerY)、などがある。
  • relation には、等値(=)、以下(≦)、以上(≧)の3種類がある。
  • view2 の属性 attr2 の◯◯倍、という指定ができるのが multiplier
  • view2 の属性 attr2 +◯◯、という指定ができるのが constant

ただし、使う際には少しだけ注意するポイントがあります。

  • attr1 と attr2 に、位置とサイズの組み合わせは指定できない。
    • 例えば、attr1: NSLayoutAttributeLeft(左側の位置), … attr2: NSLayoutAttributeWidth(幅) はエラー。
  • 位置の指定をする場合、view2 に nil を指定することはできない。
    • 当然といえば当然ですが、位置の指定では基準となるViewを指定する必要があります。

これらはVFLでも当然指定できません。(指定する方法も無いでしょうが・・・)

constraintWithItem…の持つ能力

さて、実のところ、constraintWithItem と constraintsWithVisualFormat のできることに大きな違いはありません。 しかし逆に言えば、少ないながらもVFLでは書けないレイアウトがあります。 今までに私が気づいた範囲内で VFL を超えている部分としては、次があります。

width と height を組み合わせた制約

例)幅:高さ=4:3のビュー

UIView @viewの中に、UIImageView @imageView が配置されていて、UIImageViewの幅:高さが4:3に固定される

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  @imageView.translatesAutoresizingMaskIntoConstraints = false
  @view.addConstraints([
    NSLayoutConstraint.constraintWithItem(
      @imageView,
      attribute: NSLayoutAttributeLeft,
      relatedBy:NSLayoutRelationEqual,
      toItem:@view,
      attribute:NSLayoutAttributeLeft,
      multiplier:1,
      constant:60
    ),
    NSLayoutConstraint.constraintWithItem(
      @imageView,
      attribute: NSLayoutAttributeRight,
      relatedBy:NSLayoutRelationEqual,
      toItem:@view,
      attribute:NSLayoutAttributeRight,
      multiplier:1,
      constant:-60
    ),
    NSLayoutConstraint.constraintWithItem(
      @imageView,
      attribute: NSLayoutAttributeHeight,
      relatedBy:NSLayoutRelationEqual,
      toItem:@imageView,
      attribute:NSLayoutAttributeWidth,
      multiplier:3.0/4,       # imageView の高さを、imageView 自身の幅の3/4に固定する
      constant:0
    ),
  ])

一定サイズ重なりあう様な制約

例)幅60、高さ40の領域が重なる @label と @imageView UIView @viewの中に、UILabel @label と UIImageView @imageView が配置されていて、@label の右下 60x40 の領域が下の @imageView に重なっている

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  @label.translatesAutoresizingMaskIntoConstraints = false
  @imageView.translatesAutoresizingMaskIntoConstraints = false
  @view.addConstraints([
    NSLayoutConstraint.constraintWithItem(
      @imageView,
      attribute: NSLayoutAttributeTop,
      relatedBy:NSLayoutRelationEqual,
      toItem:@label,
      attribute:NSLayoutAttributeBottom,
      multiplier:1,
      constant:-40
    ),
    NSLayoutConstraint.constraintWithItem(
      @imageView,
      attribute: NSLayoutAttributeLeft,
      relatedBy:NSLayoutRelationEqual,
      toItem:@label,
      attribute:NSLayoutAttributeRight,
      multiplier:1,
      constant:-60
    )
  ])

位置に対して multiplier を使う様な制約

例)View 全体の幅を等分するSubview UIView @viewの中に、複数の subview があって、それぞれ均等な領域に割り当てられている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
delta = 2.0 / subviews.count
centerN = delta / 2
subviews.each {|r|
  @view.addConstraints([
    NSLayoutConstraint.constraintWithItem(
      r,
      attribute:NSLayoutAttributeCenterX,
      relatedBy:NSLayoutRelationEqual,
      toItem:@view,
      attribute:NSLayoutAttributeCenterX,
      multiplier:centerN,
      constant:0
    )
    NSLayoutConstraint.constraintWithItem(
      r,
      attribute:NSLayoutAttributeCenterY,
      relatedBy:NSLayoutRelationEqual,
      toItem:@view,
      attribute:NSLayoutAttributeCenterY,
      multiplier:1,
      constant:0
    )
  ])
  centerN += delta
end

まとめ

若干複雑なconstraintWithItem…ですが、いかがでしょう。やっていることはとてもシンプルなので、読み解くにはさほど難しいものではありません。 そして、VFLだけではできないオートレイアウトの深淵を覗いてみれば、これまでは「実装上の都合」で諦めてきた 複雑なレイアウトも、チャレンジしてみる価値があるのではないでしょうか。

難しい!嫌だ!と完全拒絶する前に、一度使ってみることをおすすめします。 以外に優しいやつなんです。

おまけ

本題とはちょっとずれるので、付記として2つほど。

NSLayoutConstraint.constant の秘密

VFLを使っていると中々気づけないことがあります。 NSLayoutConstraint の ほとんどのプロパティは読み取り専用ですが、constant プロパティだけは後から変更することができるのです。 つまり、constraintWithItem…で課した制約に対して、ユーザの操作などに応じて値を変えることができます。

例)ユーザがフォーカスした時に40pxだけ大きくなるTextField

  • 初期化時
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@textField.translatesAutoresizingMaskIntoConstraints = false
# constraintWithItem は、NSLayoutConstraint を単体で返すので、このように変数に取っておくことも容易です。
@textWidthConstraint = NSLayoutConstraint.constraintWithItem(
  @textField,
  attribute: NSLayoutAttributeWidth,
  relatedBy:NSLayoutRelationEqual,
  toItem:nil,
  attribute:NSLayoutAttributeNotAnAttribute,
  multiplier:1,
  constant:120
)
@view.addConstraints([
  @textWidthConstraint
  #... 実際はより多くの制約が必要でしょうが、割愛します。
])
  • ユーザの操作時
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# TextFieldDelegateProtocol::textFieldShouldBeginEditing
def textFieldShouldBeginEditing(textField)
  # せっかくなのでアニメーションさせてみます。
  UIView.animateWithDuration(KCLRadioCheckAnimationDuration, animations: lambda {
    @textWidthConstraint.constant = 160 # 制約のconstant値だけ書き換えます。
    @view.layoutIfNeeded # 制約を変えた場合は、layoutIfNeeded を呼んでおきます。
  })
  return true
end
# TextFieldDelegateProtocol::textFieldShouldEndEditing
def textFieldShouldEndEditing(textField)
  UIView.animateWithDuration(KCLRadioCheckAnimationDuration, animations: lambda {
    @textWidthConstraint.constant = 120 # 元の幅に戻します。
    @view.layoutIfNeeded
  })
  return true
end

それでもやっぱり書きづらい

入力補完やlinterがあればさほど困ることはないんでしょうが、それでもやたらと長いクラス名、メソッド名、引数名が続いてうんざりする、という方には、拙作ながら LayoutExpressを自薦させていただきます。 RubyMotion での開発向けに、レイアウトを数式の様に書く ことができるライブラリです。

例として、先に挙げた幅:高さ=4:3のビューを LayoutExpress に置き換えたものがこちらです。

1
2
3
4
5
  layout(
    leftOf(@imageView) == leftOf(@view) + 60,
    rightOf(@imageView) == rightOf(@view) - 60,
    heightOf(@imageView) == widthOf(@imageView) * 3.0 / 4
  ])

これで皆さんが NSLayoutConstraint を使ってくれることを祈って。

この記事を書いた人kyoh

kyohです。蕎麦と鳥をこよなく愛するおでぶです。C#使いでした。

waculでは、プログラマを募集しています。

現在はプロダクトとして、課題発見から改善提案まで自動で行うWeb改善プラットフォーム「AIアナリスト」を開発中です。

waculの採用情報へ

ページトップへ