LayoutでInterface Builderを使わないレイアウト

kazutoyo
PicApp Tech Blog
Published in
13 min readMar 30, 2018

--

はじめまして。 TELLERでiOS/Androidのアプリの開発をしている@kazutoyoです。

今回はSwiftで書かれているUIのレイアウトをAndroidのレイアウトシステムのように行うライブラリ、『Layout』について紹介します。

なぜLayoutなのか

現在iOSのUIの実装は主にInterface Builderで行われているかと思います。 Interface Builderで直感的にUIを配置でき、簡単にUIを構築できるツールだと思います。 ただ、私がこれまでInterface Builderで開発する上で次のような問題がありました。

  • 複数人で開発していて、XIB/Storyboardファイルがコンフリクトすると修正がかなり難しい
  • AutoLayoutの設定など複雑になってきた
  • 似たようなViewをつくるときにコピペしづらい

特にStoryBoardのコンフリクトは複数人で開発すると起こりやすく、1画面1Storyboardのようにして、作業時もなるべく気をつけて行わなければいけませんでした。

これらを避けるためコードベースでUIを実装する方法もありますが、記述量などが実装が大変だったりぱっと見て追いづらいです。

そこで今回のLayoutを用いることで、わかりやすく、高速にUIの実装を行うことができます。

使い方

Hello, Layout

このようなレイアウトをつくってみます。

幅、高さが100%となっているUIViewの中に、ポジションをセンターにしたUILabelを配置したXMLです。

<UIView
width="100%"
height="100%">
<UILabel
font="SystemBold 30"
width="auto"
height="auto"
text="Hello, Layout!!"
center.x="parent.center.x"
center.y="parent.center.y"
/>
</UIView>

そして、UIViewControllerまたはUIViewにLayoutLoadingプロトコルを追加し、 loadLayout(named:) でレイアウトファイルを指定します。

class ViewController: UIViewController, LayoutLoading {
override func viewDidLoad() {
super.viewDidLoad()

loadLayout(
named: "Main.xml"
)
}
}

たったこれだけで先程のようにレイアウトされます!!!

Relative Layout

Androidのレイアウトシステムのように、ViewとViewとの相対的にレイアウトすることが出来ます。

次のように、Viewにidを付与し、 #idで指定することで指定したViewと相対的にレイアウトすることが出来ます。

<UIView
width="100%"
height="100%">
<UILabel
id="title"
font="title1"
width="auto"
height="auto"
text="Title"
top="50"
left="15"
/>

<UIImageView
id="userIcon"
top="#title.bottom + 14"
left="#title.left"
layer.cornerRadius="14"
clipsToBounds="true"
width="28"
height="28"
image="profile"
contentMode="scaleAspectFill"/>

<UILabel
id="username"
font="System 14"
text="kazutoyo"
textColor="#313238"
left="#userIcon.right + 10"
top="#title.bottom + 20"
right="100% - 16"
/>
<UILabel
id="description"
font="caption1"
left="15"
right="100%-15"
height="auto"
top="#username.bottom + 10"
numberOfLines="2"
text="Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
/>
</UIView>

表示結果はこのようになります。

また、IDを付けずにprevious,next,parentといった指定も可能です。

<UIView
width="100%"
height="100%">
<UILabel
id="title"
font="title1"
width="auto"
height="auto"
text="Title"
bottom="next.top - 15"
left="15"
/>
<UILabel
id="description"
font="caption1"
left="15"
right="100%-15"
height="auto"
top="parent.height / 2"
numberOfLines="2"
text="description text"
/>
</UIView>

Live Reloading

Layoutの気に入っている機能の一つがLive Reloadingです。

ReactNativeやFlutterのようにiOS Simulator上でCMD+Rを押すことで、XML上で変更したレイアウトを即時に反映させることができます。

live reloading

Constants

loadLayoutconstantsを指定することで、レイアウト上に値を渡すことが出来ます。

つぎのように背景色、タイトルのテキストカラー、タイトルのテキストをDictionaryで渡し、XMLレイアウト上でその値を使います。

loadLayout(
named: "Main.xml",
constants: [
"backgroundColor": UIColor.darkGray,
"titleColor": UIColor.white,
"titleText": "Hello, World"
]
)
<UIView
width="100%"
height="100%"
backgroundColor="backgroundColor">
<UILabel
top="40"
left="15"
right="100% - 15"
font="title1"
text="{titleText}"
textColor="titleColor"
/>
</UIView>

Outlet

XMLでレイアウトしたViewをInterface Builderを使ったときと同様にSwift上に紐付けることもできます。

次のように outlet で紐付ける変数名を指定します。

<UILabel
top="40"
left="15"
right="100% - 15"
font="title1"
textColor="#000"
outlet="titleLabel"
/>

Swift上では@objcまたは@IBOutletを指定した変数を定義しておきます。

@IBOutlet private weak var titleLabel: UILabel?

このようにすることで、Swift上にViewを紐付けることが出来ます。

Action

IBActionのように、タップしたときなどにSwiftのメソッドを呼び出すことも出来ます。

次のようにUIControlのtouchUpInsidewasPressed:を指定します。

<UIButton
center.x="parent.center.x"
center.y="parent.center.y"
normalTitle="Button"
titleColor="#000"
touchUpInside="wasPressed:"
/>

Swift上では@objcまたは@IBActionを指定することで紐付けることが出来ます。

@IBAction func wasPressed(_ button: UIButton) {
print("wasPressed!!")
}

Template

使い回すような部分をテンプレート化することもできます。

例えばこのようにタグの表示のような同じようなViewをテンプレート化しましょう。

このようなTag.xmlというテンプレートをつくります。 テンプレートにはパラメータを追加することもできます。

<UILabel
font="System 11"
textColor="#313238"
backgroundColor="#F4F6F6"
width="auto + 10"
height="auto + 10"
layer.cornerRadius="3"
clipsToBounds="true"
textAlignment="center"
text="#{tagName}">
<param name="tagName" type="String"/>
</UILabel>

そして、templateでXMLファイルを指定します。 テンプレートで指定している属性を上書きすることもできます。

<UILabel
id="horror_tag"
template="Tag.xml"
tagName="ホラー"
top="#description.bottom + 15"
left="#description.left"
/>
<UILabel
id="suspense_tag"
template="Tag.xml"
tagName="サスペンス"
top="#horror_tag.top"
textColor="#5172AD"
left="#horror_tag.right + 10"
/>

このように、Templateによってレイアウトを簡単に再利用性することができます。

Macro

macro

繰り返し使うようマージンなどの値を定義しておき使うことが出来ます。

<UIView
width="100%"
height="100%">
<macro name="TOP" value="previous.bottom + SPACING"/>
<macro name="SPACING" value="15"/>
<macro name="LEFT" value="SPACING"/>
<macro name="RIGHT" value="100% - SPACING"/>
<UILabel
top="TOP + 20"
left="LEFT"
right="RIGHT"
font="title1"
text="Hello, World!"
/>
<UILabel
top="TOP"
left="LEFT"
right="RIGHT"
font="title1"
text="Hello, PicApp!"
/>
<UILabel
top="TOP"
left="LEFT"
right="RIGHT"
font="title1"
text="Hello, TELLER!"
/>
</UIView>

RedBox

レイアウト上でエラーがあるとどこかで見たことのあるような表示になりますね(笑)

指定を間違えたときなどすぐに気がつけて安心感があります(慣れてないとビックリしますが)

外部レイアウトファイルの読み込み

外部からレイアウトファイルを読み込む loadLayout(withContentsOfURL:, constants:) メソッドがあります。 これにより、非同期でファイルを取得し、レイアウトすることが出来ます。

このあたりはAppleの規約的に大丈夫かな?と思うのですが、LayoutのFAQによるといまのところ審査には通っているらしいです。

https://github.com/schibsted/layout#faq

Q. Is Layout App Store-safe? Has it been used in production?Yes, we have submitted apps using Layout to the App Store, and they have been approved without issue.

ただRollout.ioのSDKが含まれているアプリがリジェクトされたという例もあるらしく、同じように動的にアプリの挙動を変えれるので注意が必要かもしれません。

まとめ

このように、いままでInterface Builder上で行っていたことをほぼ実現でき、よりよく実装できるようになっています。 また、Layoutは1画面や1つのViewなど少しづつ使うこともできるので、取り入れやすいライブラリとなっています。

TELLERでも一部の画面から少しづつLayoutをつかったUI作成をしていこうと思います💪

--

--