前回のつづき

今回はCustom Viewの高さが可変する場合について説明したいと思います。

サンプルコード: https://github.com/stack3/iOSAutoLayoutSamples

サンプルコードを起動してFlexible Custom Viewを選択すると以下の画面が表示されます。

001

グレー背景とその上にLabelが2つ乗っていますが、これがCustom Viewです。

Resizeボタンを押すと以下のように改行が必要な文字列がLabelに設定されて、Custom Viewの高さが変わります。

002

ResizeボタンもCustom Viewの高さに合わせて下へ移動します。

横画面も対応できています。横画面の場合は文字が1行にたくさん入るので高さが低くなります。

003

STFlexibleCustomView.xib

このファイルでCustom Viewのレイアウトをしています。

004

Viewの上にLabelを2つ配置しています。

上のLabelのConstraintは以下のようになっています。

005

Superviewと20px間隔をあけて、下のLabelと8pxの間隔をあけるようになっています。

下のLabelのConstraintは以下のとおり。

006

Superviewと20px間隔をあけて、上のLabelと8pxの間隔をあけるようになっています。

STFlexibleCustomViewクラス

flexibleCustomViewCommonInitで初期化処理を行っています。

- (void)flexibleCustomViewCommonInit
{
    //
    // 前回の記事同様にxibからViewをロードし、それをself(Superview)にaddSubviewする
    //
    _contentView = [[[NSBundle mainBundle] loadNibNamed:@"STFlexibleCustomView" owner:self options:nil] objectAtIndex:0];
    _contentView.backgroundColor = [UIColor clearColor];
    _contentView.frame = self.bounds;
    // 前回同様デフォルトで設定されているが念のため
    _contentView.translatesAutoresizingMaskIntoConstraints = YES;
    _contentView.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    [self addSubview:_contentView];

    // 各背景色を設定
    self.backgroundColor = [UIColor grayColor];
    _label1.backgroundColor = [UIColor redColor];
    _label2.backgroundColor = [UIColor blueColor];
}

このメソッドをinitWithCoder、initWithFrameで呼びます。これでInterface Builder経由で生成されたとき、プログラムから生成されたとき両方に対応できます。

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self flexibleCustomViewCommonInit];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self flexibleCustomViewCommonInit];
    }
    return self;
}

layoutSubviewsでLabel#preferredMaxLayoutWidthを設定しているコードがありますが、これに関しては後述します。

STFlexibleCustomViewController.storyboard

Custom Viewを以下のように配置しています。

007

前回の記事同様、高さのConstraintは2つ設定しています。片方は30px、もう片方は30px以上です。30pxの方はPriorityを1にしています。よって点線になっています。

008

前回同様これの意味するところは、Custom Viewが自動算出する高さ(30px以上)に任せるということです。0pxと0px以上のConstraintを設定しても良いのですが、そうするとレイアウト上Custom Viewが配置されているのが視認しづらいので、最低30pxはあるだろうから30px以上としています。視認性のためにほどほどの値以上にしているのも前回同様です。

Custom Viewの高さは2つのLabelの高さに影響して可変します。Labelの高さは文字列の長さによって可変します。Label#textを設定すると自動的にCustom Viewの高さがリサイズされ、また、同時に下に配置されたResize Buttonも下へ移動します。もちろんこれらはAuto Layoutが自動に行うもので、コードは書いていません。

STFlexibleCustomViewControllerクラス

Resizeボタンを押した時の処理です。

- (IBAction)didTapResize:(id)sender
{
    _customView.label1.text = @"12345678901234567890123456789012345678901234567890";
    _customView.label2.text = @"1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";
    // setNeedsLayoutを呼ばなくても、うまくいくが念のため呼んでおく
    [self.view setNeedsLayout];
}

Custom Viewの上のLabel(label1)、下のLabel(label2)に長い文字列を入れています。最後のsetNeedsLayoutは呼ばなくても動きますが、レイアウトが変わることは示しておいたほうが良い気がするので念のため呼んでいます。

UILabel#preferredMaxLayoutWidthを更新する必要性

STFlexibleCustomViewクラスの実装に戻ってみましょう。layoutSubviewsメソッドを以下のように実装しています。

- (void)layoutSubviews
{
    CGRect bounds = self.bounds;
    _label1.preferredMaxLayoutWidth = bounds.size.width - 20*2;
    _label2.preferredMaxLayoutWidth = bounds.size.width - 20*2;

    [super layoutSubviews];
}

preferredMaxLayoutWidthは、レイアウト上の最大幅を指定するためのプロパティです。これを正しく設定しないとAuto Layoutの自動レイアウトがうまくいかなくなる可能性があります。

以下のことを頭に入れておきましょう。

  • 縦画面と横画面では画面の幅が異なる
  • Interface BuilderでViewの上に配置したUILabelのpreferredMaxLayoutWidthは、Interface Builder上で配置した幅に自動設定される

通常、Interface Builder上で配置するViewは縦画面用のものです。STFlexibleCustomView.xibでもそのようになっています。

004

この場合、Labelは左右20pxの間隔をあけているので、Labelの幅は320 – 20*2 = 280pxとなります。つまりpreferredMaxLayoutWidthも280に自動設定されます。

もしpreferredMaxLayoutWidthを280pxのたまま横画面にしたらどうなるでしょうか。layoutSubviewsでpreferredMaxLayoutWidthを設定している部分をコメントアウトしてみます。

- (void)layoutSubviews
{
    CGRect bounds = self.bounds;
    //_label1.preferredMaxLayoutWidth = bounds.size.width - 20*2;
    //_label2.preferredMaxLayoutWidth = bounds.size.width - 20*2;

    [super layoutSubviews];
}

そしてResizeボタンを押して長い文字列を挿入して横画面にしてみましょう。

009

上のLabelは問題ありません。しかし下のLabelは上下に無駄な余白ができてしまいました。下のLabelは縦画面では4行必要になります。横画面では幅が広くなるので3行で収まります。Labelの高さの計算はpreferredMaxLayoutWidthの値に影響していて、横画面480pxを前提に計算すべきところを縦画面320pxを前提とした計算をすると、4行分の高さで3行表示されてしまい無駄な余白ができてしまいます。上のLabelは文字数がそこまで多くないので、縦画面でも横画面でも必要な行数は2行なので影響がないですが、文字の長さによっては同様に問題が起きます。

つまり問題を解決するためには画面の向きが変わった時に、それに応じてpreferredMaxLayoutWidthを設定する必要があります。layoutSubviewsはViewのサイズが変わった時に呼ばれるので、ここでpreferredMaxLayoutWidthの再設定を行うのがよいでしょう。

今回の場合は左右の余白20*2 = 40pxを考慮して、Viewの幅から40pxを引いたものを設定しています。

- (void)layoutSubviews
{
    CGRect bounds = self.bounds;
    _label1.preferredMaxLayoutWidth = bounds.size.width - 20*2;
    _label2.preferredMaxLayoutWidth = bounds.size.width - 20*2;

    [super layoutSubviews];
}

super#layoutSubviewsの後で呼んでも先に呼んでもうまく動きますが、何となくsuper#layoutSubviewsでレイアウト処理が行われる前に設定したほうが良い気がするのでこのようにしています。

つづく