AutoLayoutでViewのサイズやUIレイアウトをパーセント指定する方法


2015/3/1更新:
「必見!」とかドヤァしておきながら、なんか凄い抜けたことしていたみたいで、
文中で説明しているダミーUIViewを使わなくても、普通に親のViewとEqualWidth/EqualHeightできたみたいです。失礼しました。指摘してくださった方、ありがとうございます。詳しくはコメント欄をご覧ください。
(コメントには書きましたが、親Viewのサイズがちょっとそのままでは使いたくない値だった場合は、ダミーUIView作っていいと思います)

iOS開発者・UIデザイナー必見!

iOSのStoryboardのAutoLayout。

UIコンポーネントの幅・高さはポイント数値指定しかできない。パーセント指定できないこともないけど、それにはコードでコンストレイントをいちいち記述しないといけない。

こんな風に↓
http://saveme-dot-txt.blogspot.jp/2014/03/percentage-based-layouts-using-mostly.html(SaveMe.txt様より)

そう思っていた時期がボクにもありました。

いや、でも実はできちゃうんですよ。Storyboard上だけで幅・高さのパーセント指定が。

そのコツをお伝えしましょう。

ダミーのUIViewとEqual Widths/Equal Heightsコンストレイントを使え!

Storyboard上で設定できるコンストレイントに、Equal WidthsとEqual Heightsがあることをご存知ですか?

これは何かというと、二つ以上のUI部品を選んだ状態でのみ設定できるコンストレイントで、選んだ複数のUI部品の幅または高さを、同じ値に揃えてくれるものです。

設定値の単位は、ポイントではなく、比率です。比率ですよ。大事なので2回言いました。つまり、パーセント!

このEqual WidthsコンストレイントまたはEqual Heightsコンストレイントを上手く使えばいいのです。

さて、あるクライアントから、かなり複雑な画面レイアウトを注文されたとしましょう。しかも、それはiPhone4SからiPhone6 Plusまで、全ての機種でレイアウトが乱れず一貫していること、という条件付き。

StoryBoardには、残念ながらJavaのSwingやJavaFXにあるような、高度なレイアウトクラスはありません。コンストレイントを駆使するしか無いのです。

ベースとなる考えは、画面をUIViewで区画分けすることです。

例えば以下の様な感じに。

さて、上記の図のようなUIViewによる区画分けはどうやればいいんでしょうか? ポイント固定でやったりしては、iPhone4SからiPhone6 Plusまでスケーラブルに対応できません。
ここで、Equal Widths/Equal Heightsコンストレイントの出番です。

ではまず、UIView「A」の領域を設定することにしましょう。しかし、Equal Widths/Equal Heightsコンストレイントを設定するには、同階層に存在する「もう片方」のUI要素が必要です。

もう片方? そう、ここで出てくるのが「ダミーUIView」(勝手に命名)です。

まず、「ダミーUIView」となるUIViewをStoryBoardの画面上に配置してください。そして、以下のようにコンストレイントを設定します。

上記のコンストレイントを設定すると、高さが画面の縦サイズ(注:ステータスバー除く)と同じで、幅がゼロ、つまり面積ゼロの超細長いUIViewが、画面左端(別に右端でもOKです)にくっついた状態となります。
このUIViewの名前を、後で識別しやすいように分かりやすく「VerticalCriterionView」と設定しましょう。

その名の通り、このダミーUIViewは、ダミーでありながら、縦の長さの基準(Criterion)という重要な役割を担うのです。

さて、次にUIView「A」を配置して、画面上端にくっつけましょう。UIView「A」を選択して、以下のコンストレイトを設定します。ここでは、「Update Frames」は「None」(コンストレイントは設定するが、描画は更新しない)にしておきます。

さぁそして、いよいよEqual Heightsコンストレイントの出番です。
「VerticalCriterionView」と、UIView「A」の両方を選択して、Equal Heightsコンストレイントを設定します。

すると、UIView「A」が画面全体を覆ってしまいました(「Update Frames」を「Items of New Constraints」にしたため、描画の更新が行われた)。これは、Equal Heightsコンストレイントの設定値(Multiplier)のデフォルトが1であるためです。

つまり、ステータスバーを除いた画面の高さと同じ高さである「VerticalCriterionView」と、全く同じ高さがUIView「A」に設定されたわけです。だから、画面を覆い尽くしてしまったんですね。

さて、ここであわてず、XCode左側のビュー階層ペインから、Equal Heightsコンストレイントを選択します。

すると、XCode右側のプロパティパネルで、Equal Heightsコンストレイントの設定値(Multiplier)を変更することができます。

ここで、0.25を入力してみましょう。うまくいけば、綺麗にUIView「A」の高さが「VerticalCriterionView」(つまり、ステータスバーを除いた画面の高さ)の25%になるはずです。

もし、高さが縮むどころかもっと伸びてしまった場合は、下図のように、設定対象を入れ替えれば、ちゃんと25%になってくれます。

さて、これで、まずUIView「A」の区画設定は完了ですね。

次にUIView「B」に移りましょう。もう、勘の良い読者なら、やり方はわかりますね。「VerticalCriterionView」とUIView「B」を選択して、Equal Heightsコンストレイントを設定、コンストレイント値を0.5に設定すればよいのです。

さて、今度はUIView「C」。これもAやBと同じようにしてもよいのですが、最後の1つは手を抜くことができます。単純に以下のコンストレイントを設定すれば良いのです。

AとBの比率が決まっているなら、残りのCのサイズは自ずと決まりますからね。

入れ子にしよう&横バージョンに挑戦!

さて、今度はUIView「B」の中を、横に2分割しましょう。そのためには、UIView「B」の中に、新規UIViewを2つ配置することになります。それぞれ、UIView「B1」とUIView「B2」という名前だとしましょう。

勘の良い読者なら、横バージョンのやり方はもうわかりますね。

ダミーUIViewをまず最初に配置し(今度は横なので「HorizontalCriterionView」という名前にしましょう)、以下のようにコンストレイントを設定します。

そして、UIView「B」を配置。以下のコンストレイントを設定した後、

「HorizontalCriterionView」とUIView「B1」に対してEqual Widthsコンストレイントを設定。

コンストレイント値(Multiplier)を0.4に設定します。

UIView「B2」についても、同様にやってもいいんですが、また楽をしちゃいましょう。以下のようにコンストレイントを設定します。

これで、パーセント指定レイアウトの出来上がり! iPhone4Sから6 Plusまで、各画面サイズでもスケーラブルにレイアウトが維持されていることを確認してみましょう。バッチリですね!

UIViewだけでなく、他のUI部品にも有効

見出しですでに書いちゃってますが、このパーセント指定法はUIViewだけでなく、UILabelやUIButton、UIImageViewなど、ほとんどのUI部品に適用可能です。

フォントサイズはどうする?

さて、レイアウトはStoryboard上のみでパーセント指定できましたが、UILabelなどのフォントサイズは、各機種の画面サイズによってスケーラブルにする方法はないのでしょうか?

残念ながら、今のところはないようです。こればっかりは、コード上でフォントサイズを計算してスケールさせるしかありません。

私の場合は、StoryBoard上でUILabelなどのフォントサイズを、iPhone5の画面サイズを前提に設定しています。

それらのUILabelなどをコード側にアウトレット接続し、
-(void)viewWillAppear:(BOOL)animated
などの関数の中で、フォントサイズをスケールさせています。

具体的には、以下の様な感じです。

- (CGFloat)scaleFontSize
{
    if ([UIScreen mainScreen].bounds.size.Heights >= 700) { // 縦が700以上だったら、iPhone 6 Plus の標準解像度設定とみなす
        return 1.29375; //   (iPhone6 Plusの画面横幅)/(iPhone5の画面横幅)
    } else if ([UIScreen mainScreen].bounds.size.Heights >= 650){ // 縦が650以上だったら、iPhone 6 の標準解像度設定とみなす
        return 1.171875; //   (iPhone6の画面横幅)/(iPhone5の画面横幅)
    }

    return 1.0;
}

-(void)viewWillAppear:(BOOL)animated {
    CGFloat scale = [self scaleFontSize];

 self.titleLabel.font = [UIFont systemFontOfSize:25.0f * scale];

}

うーん。Storyboard上で同様のことができるような、もっと良い方法ないですかねぇ? ご存知の方いたら教えて下さい。

ちなみに、UIImageViewなどをコード上でスケールさせたい場合は、UIViewのtransformプロパティとCGAffineTransformMakeScale関数を使う方法がお勧めです。

さいごに

さて、いかがだったでしょうか。
上記の方法による、縦と横のパーセント指定と、UIViewの入れ子を駆使すれば、大抵のレイアウトは実現可能です。

これで、どんな複雑なUI画面でも、「全機種対応で!」とか言われても、怖くない(?)ですね!