前回の続き。

今回は、frameとAutoreszing Maskの合わせ技について。サンプルを作ったので、それを元に説明します。

サンプル: https://github.com/stack3/STProgrammaticLayoutViewSample

サンプルを起動してMenu画面のFill(OK)を選択。

f:id:eimei23:20130114121203p:plain

f:id:eimei23:20130114121212p:plain

ButtonをView全体に貼り付けています。Buttonのタイトルが中央に表示されています。画面を回転させるとButtonのサイズが自動的にリサイズされます。

今度はMenu画面のFill(NG)を選択。

f:id:eimei23:20130114121217p:plain

なんだかおかしいですね・・・。Buttonのタイトルが下にずれています。これはButtonが下へはみ出てしまっているからです。

f:id:eimei23:20130114121227p:plain

回転させたときは、さらに酷いことになります・・・

OKの方はAutoresizingMaskをきちんと設定しているのでうまくいっています。コードを見てみましょう。

STLayoutFillViewController.mの内容

- (id)initWithOK:(BOOL)isOK
{
    self = [super initWithNibName:nil bundle:nil];
    if (self) {
        _isOK = isOK;
        self.title = [NSString stringWithFormat:@"Fill(%@)", isOK ? @"OK" : @"NG"];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];
    
    CGRect bounds = self.view.bounds;
    NSLog(@"viewDidLoad self.view.bounds(%f,%f,%f,%f)", bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height);
    
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    _button = button;
    _button.frame = bounds;
    [_button setTitle:@"This label fills self.view." forState:UIControlStateNormal];
    _button.titleLabel.font = [UIFont systemFontOfSize:24.0f];
    if (_isOK) {
        _button.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    }
    [self.view addSubview:_button];
}

初期化のinitWithOKで_isOK変数にYES or NOを入れて、OKモード、NGモードどちらで動くか指定しています。viewDidLoadでは_isOKの場合、Autoresizing Maskを設定しています。

Autoresizing Maskを設定しないとなぜ正常に動かないか?

NGの方が正常に動かない理由はなんでしょうか?回転がうまくいかないのは納得できると思います。ただ初期表示はうまくいきそうなものです。なぜならself.view.boundsでSuperviewの矩形を取得して、それを代入しているからです。なぜ表示の際にサイズが異なるのでしょう。

viewDidLoadで取得したboundsをNSLogで出力しているので見てみましょう。

4-inch(iPhone 5)で動かした場合

self.view.bounds(0.000000,0.000000,320.000000,548.000000)

3.5-inch(iPhone 5以前)で動かした場合

self.view.bounds(0.000000,0.000000,320.000000,460.000000)

この高さはNavigationBarによって高さが狭くなることが考慮されていません。4-inchの場合、端末の高さは568px。3.5-inchの場合は480px。それぞれステータスバーの20pxを引いただけの548px、460pxになっています。

viewDidLoadを抜けた後に、self.view.boundsがNavigationBarを考慮した値になります。NGでButtonが下にはみ出てしまうのは、これが原因です。つまりviewDidLoadで取得したboundsを元にSubviewのframeを設定するだけでは不十分だということです。

じゃあCGRectMake(0, 0, 320, 416)とか直打ちで。どうせ縦画面しか対応しないし・・・以前はこれでも何とかなったのですが、高さの異なるiPhone 5が出てからはそうはいきません・・・

一応言っておくとiOS 6からはInterface Builder + Auto Layoutで、Autoresizing Maskより楽できます。しかしframeを直接設定したい場合や過去のコードのメンテなどでAutoresizing Maskの理解は必要になるでしょう。

ついでに言っておくとframeへ値直うちしているコードをiPhone 5対応するには、Autoresizing Maskを設定するほうが、Auto Layoutへ切り替えるより少ない工数で済むと思います。

Auto Layoutについてのチュートリアルはこちら

Autoresizing Maskの役割

Interface Builder(以下IB)だと、深く意識せずにAutoresizing Maskを設定できるのですが、プログラムから設定する場合は、きちんと理解する必要があります。

Superview(UIViewController#view)のframeが変わったとき、Subview(Button)のautoresizingMaskが設定されていると、それを元に自動的にSubviewがリサイズされます。

今回、ButtonのautoresizingMaskに代入する値(Bit値)は以下の2つ。

  • UIViewAutoresizingFlexibleWidth – Superviewの幅の伸縮に合わせて幅を伸縮する
  • UIViewAutoresizingFlexibleHeight – Superviewの高さの伸縮に合わせて高さを伸縮する

viewDidLoadでButtonのframeを設定するときに一緒にこれらを設定しています。

    _button.frame = bounds;
    if (_isOK) {
        _button.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    }

Buttonはself.view.bounds(Superview)と同じサイズで、かつ、幅と高さを伸縮するように設定されているので、常にself.view全体をおおうことになります。

さきほどviewDidLoadのself.view.boundsは実際に表示するときのboundsと異なりましたが、viewDidLoadを抜けた後にNavigationBarを考慮した高さが設定されます。そのタイミングでSubview自動リサイズが働き、Autoresizing Maskの値にしたがってリサイズされます。

ちなみに回転した時もself.view.boundsが変更されるので、このときもSubviewが自動リサイズされます。よって今回もOKの方は回転させるとButtonも合わせてリサイズされています。

viewDidLoadでのSubviewへのframeの設定も重要

UIViewAutoresizingFlexibleWidthとUIViewAutoresizingFlexibleHeightはSuperviewと同じサイズに合わせるためのものではありません。Superviewのboundsの変化に合わせて自身のサイズを合わせるものです。

今回Buttonのサイズがself.view.boundsと同じになるのは、viewDidLoadでframeに同じサイズにしているからです。

試しにButtonのframeをCGRectZeroにして起動してみましょう。

_button.frame = CGRectZero;

f:id:eimei23:20130114121331p:plain

何も表示されません。Buttonのサイズが0であるため、Superviewのboundsが変更されてもサイズを変更する必要がないと判断されているからです。

まとめ

  • viewDidLoadのself.view.boundsは実際に表示するboundsと異なることがある
  • viewDidLoadでSubviewのframeとautoresizingMaskを一緒に設定する
  • autoresizingMaskは回転時の対応もある程度できる(複雑な場合は除く)
  • 画面回転しなくてもiPhone 5対応のためにもautoresizingMask設定は必要
  • ただしiOS 6以上で最初からコードを書くならIB + Auto Layoutを使うほうが良い

その4へ続く