Xcode
iOS
Storyboard
qnoteDay 23

第三の Storyboad 記述法

More than 3 years have passed since last update.


InterfaceBuilder vs UIView()

iOS 開発者のみなさん、UI 開発に InterfaceBuilder (Storyboard/Xib) を使っていますか?

それとも Storyboard は一切使わずにコードから UI を記述していますか?

よくある実装スタイルの変遷として、


  1. InterfaceBuilder使う

     ↓

  2. どこで何が設定されているかわからない

     ↓

  3. 全てコードで UI を記述する

     ↓

  4. ViewController のコード量が肥大化

     ↓

  5. 特に AutoLayout 制約は膨大になりがち

     ↓

  6. やっぱ InterfaceBuilder 使う

みたいに一周回るパターンは多いと思います。結局のところどちらが良いかというのは宗教戦争になりがちですが、静的なレイアウトは Storyboard で定義し、動的な部分はコードで記述する、といったように臨機応変に使うのが落とし所なのでしょう。

個人的には Storyboard 派ですが、ファイルを開くだけで変更が発生したり、Git などで差分が見辛いという細かなところは不満はありますので、コードで書きたい気持ちもわからなくはないです。

AutoLayout だと、Visual Format Language という形式の記述が公式にサポートされており、コードベースで制約を記述できたり、またサードパーティ製のレイアウト記述言語もあったり、やはり皆コードで書いて可視化したいという要望はあるのだなと思いました。

さて、本題ですが、そんな「Storyboard派」と「コードで UI 書いちゃうよ派」の争いに第三勢力を提案したいと思います。

それは、

「Storyboard は XML で書いちゃうぜ派」

です。

そうです、たまに Storyboard 開くと見えちゃう膨大な XML。あの XML を書いちゃうぜ、というまさに漢(おとこ)の実装です。

だんだんふざけた方向に行ってますが、まだそこそこ正気です。


Storyboard を XML で読む

Xcode で Storyboard をうっかり差分表示すると膨大な XML が表示され、そっ閉じする人も多いと思います。でも、意外と向き合って読み解くと簡単な記述であることがわかります。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>

<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="9531" systemVersion="15C50" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="StoryboardTest" customModuleProvider="target" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

これは Single View Application のプロジェクト作成時に予め用意されている Main.storyboard の中身です。シーン(<scene>)要素の下にオブジェクト群としてビューコントローラと FirstResponder があり、その下にレイアウトガイドとビューがぶら下がっていて、、と、XMLのツリーが、InterfaceBuilder 上のツリーとほぼ一致していますね。あとはそれぞれの要素に属性値として id などの固有のプロパティが設定されているだけです。HTML みたいで簡単ですね!

スクリーンショット 2015-12-23 15.25.39.png


エディタ

Xcode 上でも Storyboard ファイルを XML として開き、直接編集することはできます。ただ、この場合、変更がリアルタイムに表示されません。オススメは、Xcode 上では InterfaceBuilder モードで表示しておき、任意の外部エディタでファイルを直接開いて編集、です。こうすれば、エディタでの変更がリアルタイムに InterfaceBuilder 上で反映され、また、記述ミスの場合もすぐに警告が出るので、文法に悩むことはありません。画面上に左右に並べると使いやすいです。

スクリーンショット 2015-12-23 15.35.04.png


XML 定義

では、XML でどのようにビューを配置するのか見てみましょう。


UIView

InterfaceBuilder 上から UIView を配置すると以下のような XML 要素が挿入されます。

<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="lhB-BI-2Xk">

<rect key="frame" x="10" y="117" width="240" height="128"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>

ふむふむ。UIView は <view> タグで記述するみたいですね!わかりやすい。プロパティとして contentMode と fixedFrame, translatesAutoresizingMaskIntoConstraints が付いていますが、UIView 由来なのでなんとなく理解できます。id はその Storyboard ファイル内で一意であれば何でも良いみたいです。

view 要素の子ノードでは、rect と color 要素が定義されています。これも直感的ですね。


UILabel

ラベルの配置は <label> タグを使用します。ラベル文字列は text 属性で。textAlignment や、adjustsFontSizeToFit などの属性もそのままのプロパティ名が連想できると思います。UIView と違い、子ノードで fontDescription でフォントの指定をします。

<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="ラベル" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xNC-fh-rQS">

<rect key="frame" x="48" y="226" width="51" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="calibratedRGB"/>
</label>


UIImageView

画像を配置する場合は UIImageView を使用しますが、XML では <imageView> タグを使用します。これも UIImageView のプロパティを連想させる属性ばかりでわかりやすいですね。画像ファイル名は image 属性でファイル名を指定します。ここでは ImageAssets 内の cat.png を使用しています。

     <imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" image="cat" translatesAutoresizingMaskIntoConstraints="NO" id="CdR-vA-Guq">

<rect key="frame" x="21" y="8" width="240" height="128"/>
</imageView>
</view>
</viewController>
〜略〜
</scenes>
<resources>
<image name="cat" width="2480" height="3508"/>
</resources>
</document>

XML 上ではその他に、UIImageView で使用している画像ファイルを <resources> タグで image 要素を記述しておく必要があるようです。(これが無いと怒られます)


AutoLayout 制約

AutoLayout の制約も XML で書けます。<constraints> タグの子ノードに <constraint> タグで記述します。<constrant> タグの属性値は NSLayoutConstraint のプロパティそのものですので、これも直感的に理解できるでしょう。

<viewController ...〜省略〜...>

<layoutGuides>
<viewControllerLayoutGuide type="top" id="topLayoutGuide"/>
<viewControllerLayoutGuide type="bottom" id="bottomLayoutGuide"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="mainView">
<subviews>
<imageView ...〜省略〜...>
<rect key="frame" x="20" y="20" width="240" height="128"/>
<constraints>
<constraint firstAttribute="width" secondItem="topImageView" secondAttribute="height" multiplier="15:8" id="FQU-uK-27Z"/>
<constraint firstAttribute="height" constant="128" id="fFj-7Q-dpS"/>
<constraint firstAttribute="width" constant="240" id="ycr-Ks-zoH"/>
</constraints>
</imageView>
</subviews>
<constraints>
<constraint firstItem="topImageView" firstAttribute="top" secondItem="topLayoutGuide" secondAttribute="bottom" id="Ndh-n1-vpP"/>
<constraint firstItem="topImageView" firstAttribute="leading" secondItem="mainView" secondAttribute="leadingMargin" id="sfx-KI-rL8"/>
</constraints>
</view>
</viewController>

上記の XML を Storyboard で見るとこうなります。

スクリーンショット 2015-12-23 17.00.21.png

関連するオブジェクトの id をわかりやすい名前にしておくとさらに可読性が上がります。InterfaceBuilder 上よりも XML の方が見やすいんじゃないでしょうか。慣れてくると XML の方が(制約を俯瞰的に見るという意味では)優れいていると思えてきます。


デメリット

Storyboard の XML は必要な記述が無いとすぐに InterfaceBuilder(Xcode) がダイアログを出して怒ります。特にUIImageView のサブビューにビューを突っ込むような暴挙をすると激おこです(Xcodeが落ちます)。

つまりApple 的には全く推奨していない記述法なので、何が起きるかわかりません。またバージョンによって互換性がなくなるかもしれません。あとドキュメントありません。記述ミスがあるとビルドが通らなくなるだけでなく最悪プロジェクトファイルそのものが開かなくなる可能性もあります。

つまりハードモードですね。わかりません。


まとめ

と、半分ネタな感じの終わり方になりましたが、まとめとしては、ゼロから Storyboard の XML を書き起こすのはまず現実的では無いということです。でも、細かな座標位置の調整やテキストの変更だけであれば XML の方が修正しやすいですし、あと、git のコンフリクト時の手動マージなどは XML での読み書きは必須ですね。意外と直感的なので Storyboard ファイルを間違って XML で開いても毛嫌いせず温かい目で読んであげて下さい!

ただし、くれぐれも自己責任でお願いします。