Developer's Blog

多解像度時代を生き抜くための Autolayout ウル技 3 選

Fenrir Advent Calendar 2014

こんにちは。共同開発部 開発担当の伊藤です。

Fenrir Advent Calendar 2014、ついに最終日となりました。

ウル技という言葉をご存じの方、どれくらいいらっしゃいますでしょうか。いわゆる"裏ワザ"の別表記で、ファミコン/スーファミ全盛期に使われた言葉です。(ちなみに"ウルテク"と読むそうです)

この時期を生き抜いた男子で、ウル技ときいてときめかない人は少ないはず!伊藤は大技林、出るたび毎回買っていました。

今年は iPhone 6 / 6 Plus の登場で、いよいよ iOS も多解像度に突入してしまいました。たぶんこの先ガンガン解像度が増える覚悟が必要になってくるはず。

iOS アプリエンジニアが多解像度時代を生き抜くために必要なスキルと言えば Autolayout。しかしながらこいつがなかなかくせ者で、うまく使いこなすにはいろいろなテクニックが必要になります。

そこで今回は、この半年でフェンリルのエンジニアが行った Autolayout の実装の中からグッと来た Autolayout のウル技を 3 つご紹介します。

項目の表示/非表示切り替え

まずよくあるパターン。状況によって特定のブロックを消したいという要求です。

Image01

Android では LinearLayout の中で visibility を gone にすれば終わりなのですが、iOS ではそうはいきません。まず思いつくのは以下のように 2 つの Constraints のアウトレットを定義して、値によって差し替えるという方法です。

Image02

@interface Demo1ViewController ()

@property (weak, nonatomic) IBOutlet UISwitch *stateSwitch;

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *sectionHeightConstraint;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *sectionBottomMarginConstraint;

@property (nonatomic) CGFloat sectionBottomMargin;
@property (nonatomic) CGFloat sectionHeight;

@end

@implementation Demo1ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // 起動時の状態を保持しておいて
    self.sectionBottomMargin = self.sectionBottomMarginConstraint.constant;
    self.sectionHeight = self.sectionHeightConstraint.constant;
}

- (IBAction)stateChanged:(id)sender
{
    // スイッチの状態で差し替える
    if (self.stateSwitch.on) {
        self.sectionView.hidden = NO;
        self.sectionHeightConstraint.constant = self.sectionHeight;
        self.sectionBottomMarginConstraint.constant = self.sectionBottomMargin;
        [self.sectionView setNeedsUpdateConstraints];
    }
    else
    {
        self.sectionView.hidden = YES;
        self.sectionHeightConstraint.constant = 0;
        self.sectionBottomMarginConstraint.constant = 0;
        [self.sectionView setNeedsUpdateConstraints];
    }
}

@end

しかし、これは 2 つの値をセットしないといけないし、一般化しづらいです。また、コンテンツの高さが変わったときに自動計算してくれるという Autolayout の良いところも使うことができません。

そこで以下のように、セクション全体を View で囲って、このコンテナとなっている View の Height に Less Than or Equal で高さをセット、下のマージンを指定している Constraint については Priority を 999 に下げます。

そして、Less Than or Equal で定義した制約を Outlet に持たせます。

Image03

すると、1 つの Constraint の Constants を CGFLOAT_MAX と 0 で切り替えるだけで表示制御ができます。

@interface Demo1ViewController ()

@property (weak, nonatomic) IBOutlet UISwitch *stateSwitch;

@property (weak, nonatomic) IBOutlet NSLayoutConstraint *sectionHeightConstraint;

@end

@implementation Demo1ViewController

- (IBAction)stateChanged:(id)sender
{
    // たった 1 つの Constrant を CGFLOAT_MAX or 0 で決め打ちするだけでセクションの表示制御ができるように!
    self.sectionHeightConstraint.constant = self.stateSwitch.on ? CGFLOAT_MAX : 0;
}

@end

Less Than or Equal や Greater Than or Equal は Autolayout の中でも特に便利だと思います。たとえば、テキストの表示される行数の上限と下限を決めるなどといった使い方が可能です。

アスペクト比の Constraints を有効活用する

次はこんな画面。UIImageView を上にセットしつつ、下に他の View があるという画面です。画像は固定の画像です。

Image04

iPhone 6 と 6 Plus は画面の大きさが異なるため、画像セットして、親 View と他の View との相対関係を指定するだけでは iPhone 6 / iPhone 6 Plus で実行したときにアスペクト比が崩れてしまいます。(iPhone 6 Plus でつぶれてしまっています)

Image05

こういうときは、UIImageView に対して、親 View との Equal Width の Constraint をつけた上でアスペクト比の Constraint をつけます。アスペクト比はセットした画像の本来のアスペクト比を指定します。これにより、アスペクト比が優先されて高さを決定されます。

Image06

実行すると、画像のアスペクト比が保持されていることがわかります。Aspect Ratio の Constraint はなかなか使いどころがないですが、これでないとどうしても解決できない場面があります。頭の片隅に入れておくと良いでしょう。

Image07

ScrollView にスティッキーな項目をつける

UIScrollView や UITableView に固定された項目をつける方法です。右下に追加ボタン出しっ放しにするとか、最近結構よく見かけると思います。

Image09

ScrollView 自体に項目を入れて、ContentOffset を監視して・・・みたいなことをしてしまいそうになりますが、固定したい View と ScrollView を同じ親の View に入れて、その親の View から相対指定するだけです。

Image08

おわりに

いかがでしたでしょうか。すでに多くのアプリを開発しているエンジニアの中には「当然じゃんこんなの」って思う方もいるかとおもいます。しかし、これらの技が実際の現場で役に立ったので、これらの技に感謝を込めながら紹介させていただきました。

まあ、大技林のウル技も当然じゃんみたいなの結構いっぱいあったので、まあそんなもんということで、軽く笑っていただければと思います。

25 日にわたってお届けした Fenrir Advent Calendar 2014 も今日でおしまい。もしまだご覧になってない記事があれば、告知エントリにリストがありますので、あわせてご覧頂ければ幸いです。

それでは、みなさま、ハッピーメリークリスマス!

フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!

Copyright © 2019 Fenrir Inc. All rights reserved.