SwiftでAutoLayoutを楽に書くには

JavaプログラマXcodeiPhoneアプリを作ってみる」の9週目です。

社内開発でSwiftでアプリを作っていて、デザイナーチームからいい感じのレイアウトが渡されてくるようになりました。今度出るはずのワイドなiPhoneを見据えて密かにStoryboardでAutolayoutを設定する練習を行っていたのですが、実作業1日目で挫折しました。マウスで小さいボタンやらラベルを選択するのが難しい上、相対表示の相手先がすぐにどこかわからなくなります。間隔を後から一律変更できるようにとかもしたいのですが、ちょっと調べてもやり方がわかりませんでした。

諦めて、SwiftでAutoLayoutを設定し出したのですが、NSLayoutConstraintのパラメータが多すぎます。とりあえず書けたとしても変更・保守できる自信がまったくありません。XML定義ではなく、古のJBuilderのごとくGUIコンポーネントを配置したら裏でコードを吐き出してくれれば、まだマシなのかもしれませんが。。皆さんどうやってレイアウトを設定しているのでしょうか?

仕方がないのAutoLayoutを設定しやすいようにするクラスを作って見ました。これで、デザイナーチームにストーリーボードでレイアウトを設定してもらう作業は完全に諦めることになります。


使い方

https://github.com/grachro/swift-layoutからダウンロードして、Layout.swiftだけもよりのプロジェクトへ入れてください。あとのファイルはデモ用です。
Layout.regsit(対象のUIView)で初めて、チェーン方式でレイアウトの相対位置を指定できるようになります。


簡単な例


こんな感じのレイアウト配置をしていみます。

let 起点 = UILabel()
起点.text = "起点"
Layout.regist(起点, container: self.view)
  .horizontalCenterInContainer()
  .verticalCenterInContainer()

まず、「起点」ラベルを中央に配置する方法です。
Layout.regist(対象となるUIView,対象となるUIViewのコンテナView)で初めて、horizontalCenterInContainer()で水平位置をコンテナの中央に、verticalCenterInContainer()で垂直位置をコンテナの中央に配置しています。コンテナとはレイアウト対象のUIViewの親Viewの事で、UIViewControllerでself.view.addSubview(aView)と書くときのself.viewにあたるものです。

コンテナViewを一々書かないといけないのが面倒ですがAutoLayoutの性質上こうなりました。regist()メソッドの内部では、view.setTranslatesAutoresizingMaskIntoConstraints(false)の後にcontainer.addSubview(view)を呼ぶお約束の処理を行っています。(お約束:iOSで柔軟に対応可能なレイアウトを作成できるAuto Layout入門)


let= UILabel()
右.text = "右"
Layout.regist(右, container: self.view)
  .left(20).fromRight(起点)
  .verticalCenterIsSame(起点)

次に「起点」ラベルの横に「右」ラベルを配置します。
left(20).fromRight(起点)のところが、「右」ラベルの左辺が「起点」ラベルの右辺から20の距離であることを指定しています。
verticalCenterIsSame(起点)で、垂直位置が「起点」ラベルと同じになります。「起点」ラベルの垂直位置はコンテナの中央なので、ここは.verticalCenterInContainer()と書いても同じはずです。


let= UILabel()
左.text = "左"
Layout.regist(左, container: self.view)
  .right(20).fromLeft(起点)
  .verticalCenterIsSame(起点)

反対側に「左」ラベルを配置します。
right(20).fromLeft(起点)で、「左」ラベルの右辺が「起点」ラベルの左辺から20の距離となります。


let= UILabel()
上.text = "上"
Layout.regist(上, container: self.view)
  .bottom(20).fromTop(起点)
  .horizontalCenterIsSame(起点)


let= UILabel()
下.text = "下"
Layout.regist(下, container: self.view)
  .top(20).fromBottom(起点)
  .horizontalCenterIsSame(起点)

同様に「上」「下」ラベルを配置します。



コンテナからの相対位置の指定方法


次に、このレイアウトを設定してみます。

let lbl左上 = UILabel()
lbl左上.text = "左上"
Layout.regist(lbl左上, container: self.view)
  .top(20).fromContainerTop()
  .left(20).fromContainerLeft()

let lbl右下 = UILabel()
lbl右下.text = "右下"
Layout.regist(lbl右下, container: self.view)
  .bottom(20).fromContainerBottom()
  .right(20).fromContainerRight()

コンテナからの相対位置を指定する場合は、.top(距離).fromTop(コンテナView)の代わりに.top(距離).fromContainerTop()とかけます。
他の3辺も同様です。



位置が同じことを指定する方法

サンプルのレイアウトイメージは先ほどと同じです。

let lbl左 = UILabel()
lbl左.text = "左"
Layout.regist(lbl左, container: self.view)
  .verticalCenterIsSame(lbl中央)
  .leftIsSame(lbl左上)

「左」ラベルの左辺は「左上」ラベルと同じですが、.left(0).fromLeft(lbl左上) と書く代わりに、.leftIsSame(lbl左上) とかけます。
同様に、.rightIsSame(対象ラベル)、.topIsSame(対象ラベル)、.bottomIsSame(対象ラベル)で各編の位置を指定できます。



長いですが、2つ目の画面イメージを設定するコードです

let lbl左上 = UILabel()
lbl左上.text = "左上"
Layout.regist(lbl左上, container: self.view)
    //.top(20).fromContainerTop()
    .top(20).fromTop(self.view)
    .left(20).fromContainerLeft()


let lbl右上 = UILabel()
lbl右上.text = "右上"
Layout.regist(lbl右上, container: self.view)
    .top(20).fromContainerTop()
    .right(20).fromContainerRight()


let lbl左下 = UILabel()
lbl左下.text = "左下"
Layout.regist(lbl左下, container: self.view)
    .bottom(20).fromContainerBottom()
    .left(20).fromContainerLeft()


let lbl右下 = UILabel()
lbl右下.text = "右下"
Layout.regist(lbl右下, container: self.view)
    .bottom(20).fromContainerBottom()
    .right(20).fromContainerRight()


let lbl中央 = UILabel()
lbl中央.text = "中央"
Layout.regist(lbl中央, container: self.view)
    .horizontalCenterInContainer()
    .verticalCenterInContainer()


let lbl左 = UILabel()
lbl左.text = "左"
Layout.regist(lbl左, container: self.view)
    .verticalCenterIsSame(lbl中央)
    .leftIsSame(lbl左上)


let lbl右 = UILabel()
lbl右.text = "右"
Layout.regist(lbl右, container: self.view)
    .verticalCenterIsSame(lbl中央)
    .rightIsSame(lbl右上)

let lbl上 = UILabel()
lbl上.text = "上"
Layout.regist(lbl上, container: self.view)
    .topIsSame(lbl左上)
    .horizontalCenterIsSame(lbl中央)

let lbl下 = UILabel()
lbl下.text = "下"
Layout.regist(lbl下, container: self.view)
    .bottomIsSame(lbl右下)
    .horizontalCenterIsSame(lbl中央)

等間隔に配置


コンテナ内限定ですが、等間隔に配置もできます。

var l1 = UILabel()
l1.text = "l1"
l1.backgroundColor = UIColor.redColor()
Layout.regist(l1, container: self.view)
    .verticalCenterInContainer()
    .width(30)
    .height(50)

var l2 = UILabel()
l2.text = "l2"
l2.backgroundColor = UIColor.greenColor()
Layout.regist(l2, container: self.view)
    .verticalCenterIsSame(l1)
    .widthIsSame(l1)
    .height(60)


var l3 = UILabel()
l3.text = "l3"
l3.backgroundColor = UIColor.blueColor()
Layout.regist(l3, container: self.view)
    .verticalCenterIsSame(l1)
    .widthIsSame(l1)
    .height(60)

Layout.horizontalEvenSpaceInCotainer(container: self.view, views: [l1,l2,l3], coverSpace: true)

Layout.horizontalEvenSpaceInCotainer(コンテナView, 並べるUIViewの配列, UIViewの周りに空白を置くか)で横方向に等間隔に並びます。



coverSpaceがFalseの場合は周りのスペースが作られません。




縦方向へはLayout.verticalEvenSpaceInCotainer(...)で並べられます。



幅、高さを固定

上の例で出てきていますが、幅と高さを固定値で指定したい場合は、.width(値)とか.height(値)で指定してください。



その他

Layoutとは直接関係ない機能もいくつかあります。


UILabelの文字を寄せる方向を指定(チェーン内で使用)

  • textAlignmentIsLeft()
  • textAlignmentIsCenter()
  • textAlignmentIsRight()


システムタイプのUIButtonを生成(チェーンとは関係なく使用します)

  • Layout.createSystemTypeBtn(title:String) -> UIButton


自動改行するのUILabelを生成(チェーンとは関係なく使用します)

  • Layout.createWordWrappingLabel(text:String) -> UILabel
  • Layout.createCharWrappingLabel(text:String) -> UILabel


ボタンのタップイベントにブロックを渡す
サンプルプロジェクト内の各UIViewContoroller内で、TouchBlocksを使っている箇所をみてください。
使い勝手がいまいちです。

最後に

動作確認はiPod toucheでランドスケープさせて行っています。
バグ等ありましたらお知らせください。