この記事はXcode 4.6.* + iOS 6のものです。Xcode 5 + iOS 7はこちら。

http://blogios.stack3.net/archives/1413

前回の続きです。今回はSuperviewの中心に固定したり、中心からの相対位置に固定して表示する方法を説明します。

前回と同じサンプルを使います。

https://github.com/stack3/STLayoutViewSample

Superviewの中心にSubviewを固定

サンプルのMenu画面のAuto LayoutセクションからCenter1を選択してください。ButtonがSuperviewの中心に表示されています。

f:id:eimei23:20130102111043p:plain

画面を回転させても中心に表示されたままです。

f:id:eimei23:20130102111051p:plain

STAutoLayoutCenter1ViewController.xibを開きましょう。そして中心に配置されているButtonを選択します。

f:id:eimei23:20130102111101p:plain

縦横十字に青いバー(Constraint)が表示されます。

Size Inspectorを見てみると2つのConstraintがあります。

f:id:eimei23:20130102111112p:plain

  • Align Center X to: Superview – Superviewの水平方向(X方向)中心に固定
  • Align Center Y to :Superview – Superviewの垂直方向(Y方向)中心に固定

この2つのConstraintによりButtonはSuperviewの中心に固定されています。間違いやすいのですが、縦に伸びている青いバーがAlign Center X to: Superview(水平方向中心)で、横に伸びている青いバーがAlign Center Y to: Superview(垂直方向中心)を示すConstraintです。

次は自分の手でこれらのConstraintを設定してみましょう。まずButtonを削除してください。

f:id:eimei23:20130102111122p:plain

まっさらになりました。

そしてButtonを縦横十字の青い点線のガイドが表示される中心位置に配置します。

f:id:eimei23:20130102111130p:plain

配置後。

f:id:eimei23:20130102111138p:plain

Size InspectorからConstraintを確認しましょう。

f:id:eimei23:20130102111150p:plain

  • Align Center X to: Superview – Superviewの水平方向(X方向)中心に固定
  • Top Space to: Superview – Superviewの上端からの間隔固定

という先ほどと違う組みわせになっています。実はInterface BuilderでSuperviewの中心に配置してもSuperview中心に固定するConstraintは自動で設定されません。Simulatorを起動して確認しましょう。

f:id:eimei23:20130102111157p:plain

f:id:eimei23:20130102111204p:plain

縦画面もおかしいですが、横画面にしたときはなんだか悲惨なことになりました・・・これは上端からの間隔が固定されてしまっているからです。水平方向中心は固定されています。

それでは垂直方向中心固定のConstraintを設定してみましょう。Buttonを選択した状態で、Align > Vertical Centersを選択します。

f:id:eimei23:20130102111410p:plain

このようにConstraintが追加されました。

f:id:eimei23:20130102111101p:plain

Size InspectorでもAlign Center Y to: Superviewが追加さているのを確認できます。

f:id:eimei23:20130102111112p:plain

再びSimulatorを起動して画面を回転させるとButtonが中心に固定されているはずです。

ActivityIndicatorViewなどはよくSuperview中心に固定表示するので、このConstraintが使えるでしょう。

プログラムからConstraintを操作する

次にSuperviewの中心位置からの間隔固定とプログラムからConstraintの間隔値を操作する方法を説明します。最初に言っておきますと、この例はあまりよいものではありません。理由は最後に説明します。

サンプルのMenu画面からCenter2を選択してください。

f:id:eimei23:20130102132337p:plain

f:id:eimei23:20130102132342p:plain

上下左右のラベルはSuperview中心からの間隔が固定されています。

これをInterface Builder + Auto Layoutだけでやるとなると難しいです。Superview中心から間隔固定というConstraintはありません。(と思うけど裏ワザあったりして)

このサンプルはAuto Layoutとプログラムを組みわせて実現しています。

Left Labelを選択してConstraintを確認。

f:id:eimei23:20130102132350p:plain

f:id:eimei23:20130102132359p:plain

  • Align Center Y to: Superview – 垂直方向中心固定
  • Leading Space to: Superview – 左端からの間隔固定

次はRight Label。

f:id:eimei23:20130102132406p:plain

f:id:eimei23:20130102132411p:plain

  • Align Center Y to: Superview – 垂直方向中心固定
  • Trailing Space to: Superview – 右端からの間隔固定

Top Label。

f:id:eimei23:20130102132417p:plain

f:id:eimei23:20130102132425p:plain

  • Align Center X to: Superview – 水平方向中心固定
  • Top Space to: Superview – 上端からの間隔固定

Bottom Label。

f:id:eimei23:20130102132439p:plain

f:id:eimei23:20130102132444p:plain

  • Align Center X to: Superview – 水平方向中心固定
  • Bottom Space to: SuperView – 下端からの間隔固定

STAutoLayoutCenter2ViewController.mを開いて、viewWillLayoutSubviewsをいったんコメントアウトしてください。

/*
- (void)viewWillLayoutSubviews
{
    CGPoint center = self.view.center;

    _leftHorizontalSpaceContraint.constant = center.x - _leftLabel.frame.size.width - _STSpaceFromCenter;
    _rightHorizontalSpaceContraint.constant = center.x - _rightLabel.frame.size.width - _STSpaceFromCenter;
    _topVerticalSpaceContraint.constant = center.y - _topLabel.frame.size.height - _STSpaceFromCenter;
    _bottomVerticalSpaceContraint.constant = center.y - _bottomLabel.frame.size.height - _STSpaceFromCenter;
}
*/

そしてSimulatorで起動してみます。

f:id:eimei23:20130102132452p:plain

f:id:eimei23:20130102132501p:plain

表示がおかしくなりましたね・・・それぞれ上端、下端、左端、右端からの間隔を固定するようになっているので、このようになります。実際は中心からの間隔を固定しなければなりません。

そこで今回はプログラムで何とかしています。先ほどコメントアウトした内容を元に戻してください。

- (void)viewWillLayoutSubviews
{
    CGPoint center = self.view.center;

    _leftHorizontalSpaceContraint.constant = center.x - _leftLabel.frame.size.width - _STSpaceFromCenter;
    _rightHorizontalSpaceContraint.constant = center.x - _rightLabel.frame.size.width - _STSpaceFromCenter;
    _topVerticalSpaceContraint.constant = center.y - _topLabel.frame.size.height - _STSpaceFromCenter;
    _bottomVerticalSpaceContraint.constant = center.y - _bottomLabel.frame.size.height - _STSpaceFromCenter;
}

次にSTAutoLayoutCenter2ViewController.hを確認してください。

    IBOutlet __weak UILabel *_leftLabel;
    IBOutlet __weak NSLayoutConstraint *_leftHorizontalSpaceContraint;
    IBOutlet __weak UILabel *_rightLabel;
    IBOutlet __weak NSLayoutConstraint *_rightHorizontalSpaceContraint;
    IBOutlet __weak UILabel *_topLabel;
    IBOutlet __weak NSLayoutConstraint *_topVerticalSpaceContraint;
    IBOutlet __weak UILabel *_bottomLabel;
    IBOutlet __weak NSLayoutConstraint *_bottomVerticalSpaceContraint;

IBOutletで定義されているNSLayoutConstraint型の変数がありますが、これらはそれぞれのConstraintに対応しています。

  • _leftHorizontalSpaceContraint – Left Labelの左端からの間隔固定
  • _rightHorizontalSpaceContraint – Right Labelの右端からの間隔固定
  • _topVerticalSpaceContraint – Top Labelの上端からの間隔固定
  • _bottomVerticalSpaceContraint – Bottom Labelの下端からの間隔固定

Interface Builder上のConstraintとIBOutlet変数とのひも付けはSubviewと同じ要領でできます。

Subviewを選択してConstraintを示す青いバーを表示されている状態にして、File’s Ownerのウィンドウからびよーんっと線を伸ばしてIBOutlet定義された変数とリンクします。

f:id:eimei23:20130102132638p:plain

このようにして、それぞれのConstraintをリンクすれば、プログラムから操作できます。NSLayoutConstraintにはconstantというプロパティがあります。このプロパティで間隔のピクセル値を設定できます。

さきほどのコードを見てみましょう。

#define _STSpaceFromCenter 50
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
- (void)viewWillLayoutSubviews
{
    CGPoint center = self.view.center;

    _leftHorizontalSpaceContraint.constant = center.x - _leftLabel.frame.size.width - _STSpaceFromCenter;
    _rightHorizontalSpaceContraint.constant = center.x - _rightLabel.frame.size.width - _STSpaceFromCenter;
    _topVerticalSpaceContraint.constant = center.y - _topLabel.frame.size.height - _STSpaceFromCenter;
    _bottomVerticalSpaceContraint.constant = center.y - _bottomLabel.frame.size.height - _STSpaceFromCenter;
}

Superviewの中心座標を取得して、それぞれのLabelが中心位置から50pxになるように計算して、constantの値を設定しています。viewWillLayoutSubviewsはviewDidLoadの後や画面が回転する時に呼ばれます。

最初に言ったように、この方法はあまりおすすめできません。その理由を説明します。

Left Labelを左に移動して左端の間隔を変えてみましょう。その後、File’s Ownerのウィンドウを見ると、IBOutletとのリンクが切れています。

f:id:eimei23:20130102132510p:plain

間隔を変えるとConstraintのオブジェクトが再生成されて切れてしまうのでしょうか・・・うっかり触ってしまっただけで、バグになりかねないのは怖いですね。保守性に問題ありです。ここはXcodeのバージョンが上がった時に直るかもしれませんが・・・

解決方法として中心に透明なSuperviewを配置して、それぞれのLabelはそこからの間隔を固定するConstraintを持つ方法もあります。ただこういうケースでAuto Layoutを使う必要があるのか自分は疑問があります。本サンプルのSet FrameセクションのCenter2も同じ動作をしますが、こちらはAuto Layoutを使わずにframe設定する方法でやっています。

STSetFrameCenter2ViewController.mのコード

- (void)viewDidLayoutSubviews
{
    CGSize boundsSize = self.view.bounds.size;
    CGPoint center = self.view.center;

    CGRect leftLabelFrame;
    leftLabelFrame.size = [_leftLabel sizeThatFits:boundsSize];
    leftLabelFrame.origin.x = center.x - leftLabelFrame.size.width - _STSpaceFromCenter;
    leftLabelFrame.origin.y = center.y - leftLabelFrame.size.height / 2;
    _leftLabel.frame = leftLabelFrame;

    CGRect rightLabelFrame;
    rightLabelFrame.size = [_rightLabel sizeThatFits:boundsSize];
    rightLabelFrame.origin.x = center.x + _STSpaceFromCenter;
    rightLabelFrame.origin.y = center.y - rightLabelFrame.size.height / 2;
    _rightLabel.frame = rightLabelFrame;

    CGRect topLabelFrame;
    topLabelFrame.size = [_topLabel sizeThatFits:boundsSize];
    topLabelFrame.origin.x = center.x - topLabelFrame.size.width / 2;
    topLabelFrame.origin.y = center.y - topLabelFrame.size.height - _STSpaceFromCenter;
    _topLabel.frame = topLabelFrame;

    CGRect bottomLabelFrame;
    bottomLabelFrame.size = [_bottomLabel sizeThatFits:boundsSize];
    bottomLabelFrame.origin.x = center.x - bottomLabelFrame.size.width / 2;
    bottomLabelFrame.origin.y = center.y + _STSpaceFromCenter;
    _bottomLabel.frame = bottomLabelFrame;
}

どのみち配置に関するコードを書かなければいけないのであれば、Interface Builder自体使わないのも手だと自分は思っています。その方がコードの保守性が高い場合もあるでしょう。Interface Builderとコード両方に配置に関することが分散することを自分は好みません。

Auto LayoutはSuperviewの外枠との間隔や、Subviewどうしの間隔を決めることで機能するもので、それ以外では適さない場合もあると思います。frameを設定して配置する方が良い場合もあるでしょう。

iPhone 5とiOS 6が同時にリリースされ、サイズが異なる端末に対応するためにAuto Layout必須のような認識があるようですが、適材適所ではないでしょうか?Interface Builderを使うこと自体を含めて。frame設定によるSubview配置もSuperviewのbounds.sizeを取得して、その範囲に収まるように配置すれば異なる画面サイズに対応できるはずですから。

その5へつづく