前回の続きです。今回は水平方向にSubviewを並べるときの解説です。

最初に言っておくと、この件はInterface Builder + Auto Layoutでやるとすごく楽です。その記事もあるのでどうぞ。

サンプルのMenu画面からHorizontalを選択しましょう。

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

f:id:eimei23:20130120152448p:plain

f:id:eimei23:20130120152509p:plain

TextField + Buttonの組み合わせとLabel + TextFieldの組み合わせです。TextFieldは画面を回転させると幅が可変します。

コードを見てみましょう。

STLayoutHorizontalViewController.mの内容

- (void)viewDidLoad {
  [super viewDidLoad]; 
  〜〜〜〜〜〜〜〜〜〜〜〜〜〜 
  [self layoutSubviews]; 
}

今回はviewDidLoadで各SubviewをinitWithFrame:CGRectZeroで生成して、最後にlayoutSubviewsというメソッドでまとめてframeとautoresizingMaskの設定をしています。

- (void)layoutSubviews
{
    CGSize boundsSize = self.view.bounds.size;

    CGRect searchButtonFrame;
    searchButtonFrame.size = boundsSize;
    searchButtonFrame.size = [_searchButton sizeThatFits:searchButtonFrame.size];
    searchButtonFrame.size.height = 33;
    searchButtonFrame.origin.x = boundsSize.width - 20 - searchButtonFrame.size.width;
    searchButtonFrame.origin.y = 20;
    _searchButton.frame = searchButtonFrame;
    _searchButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;

    CGRect searchTextFieldFrame;
    searchTextFieldFrame.size.width = searchButtonFrame.origin.x - 20 - 8;
    searchTextFieldFrame.size.height = 30;
    searchTextFieldFrame.origin.x = 20;
    searchTextFieldFrame.origin.y = _searchButton.center.y - searchTextFieldFrame.size.height / 2;
    _searchTextField.frame = searchTextFieldFrame;
    _searchTextField.autoresizingMask = UIViewAutoresizingFlexibleWidth;

    CGRect nameLabelFrame;
    nameLabelFrame.size = boundsSize;
    nameLabelFrame.size = [_nameLabel sizeThatFits:nameLabelFrame.size];
    nameLabelFrame.size.height = 30;
    nameLabelFrame.origin.x = 20;
    nameLabelFrame.origin.y = searchButtonFrame.origin.y + searchButtonFrame.size.height + 8;
    _nameLabel.frame = nameLabelFrame;

    CGRect nameTextFieldFrame;
    nameTextFieldFrame.origin.x = nameLabelFrame.origin.x + nameLabelFrame.size.width + 8;
    nameTextFieldFrame.origin.y = nameLabelFrame.origin.y;
    nameTextFieldFrame.size.width = boundsSize.width - nameTextFieldFrame.origin.x - 20;
    nameTextFieldFrame.size.height = 30;
    _nameTextField.frame = nameTextFieldFrame;
    _nameTextField.autoresizingMask = UIViewAutoresizingFlexibleWidth;

    CGRect longTextButtonFrame;
    longTextButtonFrame.size = boundsSize;
    longTextButtonFrame.size = [_longTextButton sizeThatFits:longTextButtonFrame.size];
    longTextButtonFrame.origin.x = boundsSize.width - longTextButtonFrame.size.width - 20;
    longTextButtonFrame.origin.y = nameLabelFrame.origin.y + nameLabelFrame.size.height + 8;
    _longTextButton.frame = longTextButtonFrame;
    _longTextButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;
}

各種Subviewの変数名

  • _searchButton – Search Button
  • _searchTextField – Search Button左隣のTextField
  • _nameLabel – Nameと表示するLabel
  • _nameTextField – _nameLabelの右隣のTextField
  • _longTextButton – Long Text Button。これは最後に説明します

間隔

  • Superviewの端との間隔20px
  • Subviewどうしの間隔8px

のようにしています。

autoresizingMaskプロパティ

  • _searchButtonはSuperview左端との間隔が画面回転によって可変するので、UIViewAutoresizingFlexibleLeftMargin
  • _searchTextFieldは画面回転によって幅が可変するので、UIViewAutoresizingFlexibleWidth
  • _nameLabelは画面回転によって間隔や幅は変わらないので、何も指定しない
  • _nameTextFieldは画面回転によって幅が可変するので、UIViewAutoresizingFlexibleWidth
  • _longTextButtonはSuperview左端との間隔が画面回転によって可変するので、 UIViewAutoresizingFlexibleLeftMargin
のようになっています。

ButtonやLabelのテキストが変更されたとき

Long Text Buttonを押してみましょう。

f:id:eimei23:20130120154356p:plain

このButtonを押すと_searchButtonのtitleと_nameLabelのtextが長いテキストに変更するように実装してあります。現状は幅が変わらず長い文字は省略されてしまいます。

もちろん理想は以下のようになることです。

  • _searchButton、_nameLabelの幅がテキストが収まるように広くなる
  • その分、隣のTextFieldは幅が狭くなる

これらを実現するためにViewControllerにゴリゴリとコードを書いてしまうと見栄えが悪くなります。見通しが良くて簡単にできる方法を使いたいと思います。

サンプルのMenu画面からHorizontal2を選択しましょう。前回のHorizontalと同じ画面が表示されますが、Long Textボタンを押したときは以下のようになります。

f:id:eimei23:20130120155014p:plain

f:id:eimei23:20130120155023p:plain

うまくいっていますね。

実は今回はUIViewクラスのsubclassを作り、そこでSubviewの配置をまとめて行なっています。そのクラス名はSTLayoutHorizontal2Viewです。

STLayoutHorizontal2View.hの内容

@interface STLayoutHorizontal2View : UIView {
    __strong UITextField *_searchTextField;
    __strong UIButton *_searchButton;
    __strong UILabel *_nameLabel;
    __strong UITextField *_nameTextField;
    __strong UIButton *_longTextButton;
}
@property (strong, nonatomic, readonly) UIButton *searchButton;
@property (strong, nonatomic, readonly) UILabel *nameLabel;
@property (strong, nonatomic, readonly) UIButton *longTextButton;

@end

STLayoutHorizontalViewのinitWithFrameでSubviewの生成、layoutSubviewsでSubviewのframeの設定を行なっています。実装内容はSTLayoutHorizontalViewController.mのviewDidLoad、layoutSubviewsとほとんど同じです。若干異なる重要な箇所があるのですが、それは後で述べます。

UIView#layoutSubviewsメソッド

UIViewには元々layoutSubviewsメソッドが定義されていて、Subviewの配置はこのメソッド内で行うのがお約束となっています。基本的にUIViewのframeが変更されるなどしたときに呼ばれます。明示的に呼びたいときは、UIView#setNeedsLayoutを呼びます。直接layoutSubviewsを呼んではいけないので注意。

※ UIViewControllerにはlayoutSubviewsというメソッドはありません。STLayoutHorizontalViewControllerで定義したのは、あくまで自前です。混乱なきよう・・・

STLayoutHorizontal2ViewControllerでSTLayoutHorizontal2Viewを配置しています。

STLayoutHorizontal2ViewControllerの内容

- (void)viewDidLoad
{
    [super viewDidLoad];

    CGRect bounds = self.view.bounds;

    self.view.backgroundColor = [UIColor whiteColor];

    STLayoutHorizontal2View *customView = [[STLayoutHorizontal2View alloc] initWithFrame:bounds];
    _customView = customView;
    _customView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    [_customView.longTextButton addTarget:self action:@selector(longTextButtonDidTap) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:_customView];
}

すっきりしましたね。frameをSuperviewのboundsにして、autoresizingMaskにUIViewAutoresizingFlexibleWidthとUIViewAutoresizingFlexibleHeightを設定していることに注目。つまり、STLayoutHorizontal2View(_customView)は、初期状態も画面回転した時もSuperviewと同じサイズになるということです。

さきほどSTLayoutHorizontal2View#layoutSubviewsは、STLayoutHorizontalViewController#layoutSubviewsの時と若干実装が異なると言いました。実はSTLayoutHorizontal2View#layoutSubviewsでは、SubviewのautoresizingMaskの設定を以下のようにコメントアウトしてあります。

 _searchButton.frame = searchButtonFrame; 
  // _searchButton.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin;

なぜautoresizingMaskの設定をせずにうまく配置されるのでしょうか?それは画面を回転させるとSTLayoutHorizontal2View以下のように処理されるからです。

  • STLayoutHorizontal2ViewのautoresizingMaskにしたがってframeが再設定される
  • つまりSuperviewと同じサイズになる
  • その後、layoutSubviewsが呼ばれて、Subviewの再配置が行われる

つまり回転のたびにlayoutSubviewsが呼ばれて、Subviewのframeを再設定するのでautoresizingMaskの設定はしなくても良いのです。

次にLong Text Buttonを押した時の処理です。STLayoutHorizontal2ViewControllerでイベントを実装しています。

- (void)longTextButtonDidTap
{
    NSString *longText = @"Looooong text";
    [_customView.searchButton setTitle:longText forState:UIControlStateNormal];
    _customView.nameLabel.text = longText;
    [_customView setNeedsLayout];
}

Buttonのtitleを変更した後にSTLayoutHorizontal2View#setNeedsLayoutを呼んでいます。これによりlayoutSubviewsが呼ばれてSubviewの再配置が行われます。

もう一度、STLayoutHorizontal2View#layoutSubviewsを見てみましょう。_searchButtonと_nameLabelのsizeThatFitsメソッドを呼んでいる箇所を注目。

 CGRect searchButtonFrame; 
  searchButtonFrame.size = boundsSize;
  searchButtonFrame.size = [_searchButton sizeThatFits:searchButtonFrame.size];
  searchButtonFrame.size.height = 33; 
  searchButtonFrame.origin.x = boundsSize.width - 20 - searchButtonFrame.size.width;   
  searchButtonFrame.origin.y = 20; 
  _searchButton.frame = searchButtonFrame;
  〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 
  CGRect nameLabelFrame; 
  nameLabelFrame.size = boundsSize; 
  nameLabelFrame.size = [_nameLabel sizeThatFits:nameLabelFrame.size];
  nameLabelFrame.size.height = 30; 
  nameLabelFrame.origin.x = 20; 
  nameLabelFrame.origin.y = searchButtonFrame.origin.y + searchButtonFrame.size.height + 8;
  _nameLabel.frame = nameLabelFrame;

sizeThatFitsはButtonならtitle、Labelならtextに合わせて最適なサイズを返します。これによりtitleやtextが変わった時も最適なレイアウトになるようにSubviewが配置されます。

このように複雑なSubview配置はUIViewをsubclassにして、回転した時、内容の変化などを考慮して、layoutSubviewsを実装しておくとスッキリすると思います。逆に内容の変化が起きるたびにframe設定するような実装だと処理が分散してわかりづらくなるでしょう。layoutSubviewsにまとめておく利点は、setNeedsLayoutを呼べば、いつでもSubviewの再配置ができることです。

おまけ

サンプルのMenu画面からHorizontal3を選択肢ましょう。これもHorizontal2同様に回転やLong Text Buttonの件も正常に動作します。このサンプルはHorizontal2のようにViewのSubclassを作らずに実現しています。

ViewControllerにviewDidLayoutSubviewsというメソッドがあります。Horizontal3では、viewDidLayoutSubviewsでSubviewのframe設定を行っています。このメソッドは、主に以下のタイミングで呼ばれます。

  • viewDidLoadが呼ばれた後
  • 回転した後
  • ViewController#viewに対してsetNeedsLayoutを呼んだ後

ただviewDidLayoutSubviewsはそのメソッド名を見て分かるように、本来Subviewのレイアウト(配置)が終わった後に処理すべきものを実装するメソッドです。SubviewのAutoresizingMaskによる自動再配置は終わった後にviewDidLayoutSubviewsが呼ばれます。

よってviewDidLayoutSubviewsでSubviewの配置(レイアウト)を行うのは変ですが、例えば既存のコードを端末サイズの異なるiPhone 5対応したい場合、Subviewのframe設定の処理を、ごっそりviewDidLayoutSubviewsに移してしまうと楽に対応できたりします。時間がないときなどはこれで対応するのも手かもしれません。もちろん、ちゃんと作りなおすのがベストなので自己判断で。

その6へ続く