Objective-C
iPhone
iOS
iOSDay 1

IB/Storyboard使わない派のlayoutSubviewsによるレイアウト調整

More than 3 years have passed since last update.


はじめに

iOSアプリ開発でInterfaceBuilderやStoryboardを使うか使わないかというのは宗教論争の火種の一つとなっていますが、自分の場合なるべく使いたくない派です。

画面回転やiPhone/iPadユニバーサル対応、そしてiOS7のステータスバー周りの仕様変更など、やり方を調べるよりささっとコードで調整できる、そして現状アプリがどういう仕様になっているかが一目瞭然なのはコードの方ではないかと思います。

コードでレイアウト調整をする場合、UIViewのサブクラスをViewController毎に用意し、その中でlayoutSubviewsをオーバーライドするとわかりやすく書くことができます。


ViewControllerでレイアウトする良くない例

自分は3年間くらいこのやり方でやっていたのですが(恥ずかしながらlayoutSubviewsを知らなくてもiOSアプリは書けちゃうものです…)

経験上ViewControllerに対するViewクラスは面倒臭がらずに定義した方がいいということがわかりました。

ViewControllerでViewの制御、回転時の対応や全画面表示、アニメーション等いろいろやっていくと、どんどん複雑で読みにくいコードになってしまいます。


HogeViewController.m

-(void) loadView {

UIView *view = [[UIView alloc] init];

_subView = [[UIView alloc] init];

// サブビューのレイアウト調整
_subView.frame = CGRectMake(50,50,50,50);

[view addSubview:_subView];

self.view = view;
}

// 画面回転時
-(void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];

// こんなことしてたら後々大変なことに...
_subView.frame = CGRectMake(...);
}



ViewControllerからViewのコードを切り離すべし

ViewControllerは理想的には極限まで薄くするのがいいと思います。

ビジネスロジックはModelに書く、汎用処理はヘルパークラスや関数にする、DataSourceは切り離す、などいろいろありますが、Viewも別クラスに切り離しましょう。

HogeViewControllerにはHogeViewを対応させ、ViewControllerがViewのインスタンスを持つようにします。

そしてViewControllerでサブビューのframeをいじっていた部分を、ViewのlayoutSubviewsに移動します。


HogeViewController.m

@implementation HogeViewController

-(id) init {
if (self = [super init]) {
// ViewControllerに対応するViewを保持
_hogeView = [[HogeView alloc] init];
}
return self;
}

-(void) loadView {
self.view = _hogeView;
}

@end



HogeView.m

@implementation HogeView

-(id) init {
if (self = [super init]) {
_subView = [[UIView alloc] init];
}
return self;
}

-(void) layoutSubviews {
[super layoutSubviews];

// レイアウト調整はここでやる
_subView.frame = CGRectMake(50,50,50,50);
}

@end


イベントハンドラの繋ぎ部分をどうするかは好みがわかれるかと思いますが、自分の場合はイベント定義が必要なコンポーネントはパブリックプロパティにしてしまって、ViewControllerから直接イベント登録することが多いです。


layoutSubviewが呼ばれるタイミング

ViewをaddSubviewした時、Viewのframeを変更した時(親ビューのlayoutSubviews経由、画面回転時など)に呼ばれます。

大抵意識することなく必要な時に呼ばれるますが、任意のタイミングでsetNeedsLayoutを呼べば手動で実行できます。

Viewに動的にサブビューを追加する場合などはサブビューをaddSubviewした後にViewのsetNeedsLayoutを呼びます。


HogeView.m

-(void) setContentsView:(UIView*)contentsView {

[_bodyView removeFromSuperview];

_contentsView = contentsView;

[self addSubview:contentsView];

// layoutSubviewsを明示実行
[self setNeedsLayout];
}



上下左右中央の寄せ

サブビューのレイアウトを親ビューのサイズを使って相対的に定義することで、画面回転による縦横にもスムーズに対応することができます。

-(void) layoutSubviews {

[super layoutSubviews];

float width = self.bounds.size.width;
float height = self.bounds.size.height;

// 間隔(padding)を空ける
float pad = 10;

// 左上
_leftTopView.frame = CGRectMake(pad, pad, 100, 100);
// 中上
_centerTopView.frame = CGRectMake((width - 100) / 2, pad, 100, 100);
// 右上
_rightTopView.frame = CGRectMake(width - 100 - pad, pad, 100, 100);
// 左中
_leftMiddleView.frame = CGRectMake(pad, (height - 100) / 2, 100, 100);
// 中中
_centerMiddleView.frame = CGRectMake((width - 100) / 2, (height - 100) / 2, 100, 100);
// 右中
_rightMiddleView.frame = CGRectMake(width - 100 - pad, (height - 100) / 2, 100, 100);
// 左下
_leftBottomView.frame = CGRectMake(pad, height - 100 - pad, 100, 100);
// 中下
_centerBottomView.frame = CGRectMake((width - 100) / 2, height - 100 - pad, 100, 100);
// 右下
_rightBottomView.frame = CGRectMake(width - 100 - pad, height - 100 - pad, 100, 100);
}


よくあるヘッダー/コンテンツ/フッターというレイアウト

上にナビゲーションバー等のヘッダー部分があって、下にタブバー等のフッター部分があって、残りはコンテンツ、というよくあるレイアウトは以下のように書けます。

-(void) layoutSubviews {

[super layoutSubviews];

float width = self.bounds.size.width;
float height = self.bounds.size.height;

// 固定の高さのヘッダー(UINavigationBar等)
_headerView.frame = CGRectMake(0, 0, width, 44);

// 固定の高さのフッター(UITabBar,UIToolbar等)
_footerView.frame = CGRectMake(height - 44, 0, width, 44);

// 可変の高さのコンテンツ(UITableView,UIScrollView等)
_contentView.frame = CGRectMake(
_headerView.bounds.size.height,
0,
width,
height - _headerView.bounds.size.height - _footerView.bounds.size.height
);
}


iPhone/iPadユニバーサル用のレイアウト調整

iPhoneサイズに適したレイアウトとiPadサイズに適したレイアウトは異なるので、分岐が必要になっていきます。IBを使う場合はiPhone用とiPad用のxibファイルを用意したりメンテナンス性に問題がありましたが、layoutSubviewsはシンプルに対応できます。


HogeView.m

-(void) layoutSubviews {

[super layoutSubviews];

float width = self.bounds.size.width;
float height = self.bounds.size.height;

BOOL isPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);

// iPadの場合は高さ300、iPhoneの場合は高さ100
float hogeH = (isPad) ? 300 : 100;
_hogeView.frame = CGRectMake(x, y, hogeH, width);
}



iOS6以前/iOS7共存のレイアウト調整

iOS7対応でステータスバーの分ビューが上にずれる!とか面倒な自体が発生しても落ち着いて対応することができます。


HogeView.m

-(void) layoutSubviews {

[super layoutSubviews];

float width = self.bounds.size.width;
float height = self.bounds.size.height;

BOOL isOS7Later = [[UIDevice currentDevice].systemVersion floatValue] >= 7.0f

//注: 縦画面専用です
float sbarH = [UIApplication sharedApplication].statusBarFrame.size.height;

// iOS7でステータスバーに重ならないようにする
float navbarY = isOS7Later ? sbarH : 0;
self.navbar.frame = CGRectMake(0, navbarY, width, 44);
}



脱autoresizingMask

layoutSubviewsでレイアウト調整を行うと決めたら、autoresizingMaskは混乱の元になるので削除する。

単純な例の場合はautoresizingMaskでも十分可能ですが、layoutSubviewsを使う方法もさほどコストはかからない上に柔軟性・拡張性に優れるため、layoutSubviews方式で統一しています。


そしてAutoLayoutへ… (iOS6以降)

諸事情によりiOS5も面倒を見る必要があるため、今のところAutoLayoutは使っていません。

試しにStoryboardで適当にいじってみましたが何が起こってるのかよくわからない…orz