前回のつづき

前回でintrinsicContentSizeで正しいサイズを返せば、うまくスクロールすることを説明しました。今回は、

  • スクロールするViewのサイズが可変する場合
  • 横画面対応

について説明します。

サンプルコード

https://github.com/stack3/iOSAutoLayoutSamples

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

ss01

画像表示(150×150)+複数行文字列(サイズ可変)の構成になっています。

縦方向に下までスクロール。

ss02

横画面も対応しています。

ss03

ss04

横画面の時は縦方向のスクロールサイズが縦画面の時より低くなります。

STVerticalScrollContentView.xib

スクロールするViewのレイアウトを行っています。

ss06

UIImageViewのConstraintは以下のとおり。

ss07

幅と高さを150pxにして、水平方向中心表示、上端の間隔は20pxです。

UILabelのConstraintは以下のとおり。

ss08

上のUIImageViewとの間隔8px、左右下は20pxです。

Constraintの設定方法がよくわからない人は、Auto Layoutのチュートリアルを参考にしてください。

STVerticalScrollContentViewクラス

スクロールするViewとなるクラスです。このクラスで行うべきことは、xibのViewをロードし、intrinsicContentSizeが正しいサイズを返すようにすることです。

初期化メソッドでxibからViewをロードし、それをSubviewとしてView全体に貼り付けています。

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

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

- (void)verticalScrollContentViewCommonInit
{
    // xibからViewをロードし、Subviewとして貼り付ける。
    // ロードしたViewはcontentViewプロパティへ入れる。
    _contentView = [self st_loadAndAddContentViewFromNibNamed:@"STVerticalScrollContentView"];
}

st_loadAndAddContentViewFromNibNamedメソッドの実装。

- (UIView *)st_loadAndAddContentViewFromNibNamed:(NSString *)nibNamed
{
    UIView *view = [[[NSBundle mainBundle] loadNibNamed:nibNamed owner:self options:nil] objectAtIndex:0];
    view.frame = self.bounds;
    view.translatesAutoresizingMaskIntoConstraints = YES;
    view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
    [self addSubview:view];
    return view;
}

Custom Viewクラスでxibをロードし全面に貼り付ける件は詳しくはこちらで説明しています。

Auto LayoutでCustom Viewを作る

次に横画面対応です。layoutSizeというプロパティを用意して、そこにCustom Viewの然るべきサイズを入れるようにしています。

@property (nonatomic) CGSize layoutSize;

画面回転して幅が変わった時の処理をするメソッドです。layoutSizeを計算します。後で述べますが、widthにScrollViewの幅が渡されます。

- (void)setLayoutWidth:(CGFloat)width
{
    // 幅の設定
    _layoutSize.width = width;
    // Labelのレイアウト幅は、_layoutSize.widthから左右のマージン分を引いたもの。
    _textLabel.preferredMaxLayoutWidth = _layoutSize.width - 20*2;
    // systemLayoutSizeFittingSizeでConstraintのルールに従って高さを自動計算する。
    // selfではなく_contentViewであることに注意。
    _layoutSize.height = [_contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
    // 小数点を四捨五入する
    _layoutSize.height = round(_layoutSize.height);
    // intrinsicContentSizeが変わったことをAuto Layoutに知らせる
    [self invalidateIntrinsicContentSize];
}

widthを受け取らず以下のようにすることも可能です。

// superviewはUIScrollView。
width = superview.frame.width;

ですが、個人的にはsuperviewがScrollViewという前提で書かない方が設計的には良いかと思います。

_layoutSize.heightをroundして小数点以下が0になるようにしている箇所について。これをしないと上端もしくは下端までスクロールした時にbounceしなくなるバグが発生することがあります。たとえばsystemLayoutSizeFittingSizeが返す値が「300.5」のようなときに、このようなバグが発生します。「301」になるとbounceするようになります。

intrinsicContentSizeはlayoutSizeプロパティを返します。

- (CGSize)intrinsicContentSize
{
    // この中でsystemLayoutSizeFittingSizeを呼ばないこと。
    return _layoutSize;
}

ちょっと回りくどい感じがしますが、intrinsicContentSizeでsystemLayoutSizeFittingSizeを呼ぶと、その中でintrinsicContentSizeを呼び無限ループになるので、このようにしています。

STVerticalScrollViewController.storyboard

画面のレイアウトです。UIScrollViewを全面に貼り付け、上下左右の間隔を0pxとするConstraintを設定してあります。

ss09

STVerticalScrollViewControllerクラス

viewDidLoadの前半部分。内容は前回で説明したとおりです。この辺りはお約束です。

- (void)viewDidLoad
{
    [super viewDidLoad];

    // スクロールの中身となるView(STVerticalScrollContentView)の生成
    _contentView = [[STVerticalScrollContentView alloc] initWithFrame:self.scrollView.bounds];
    // Constraintは自分で設定するのでNO
    _contentView.translatesAutoresizingMaskIntoConstraints = NO;
    // Subviewとして追加
    [_scrollView addSubview:_contentView];
    //
    // Constraintの設定。
    // STVerticalScrollContentViewの上下左右の間隔0pxとする
    //
    [_scrollView addConstraint:[NSLayoutConstraint constraintWithItem:_contentView
                                                            attribute:NSLayoutAttributeLeading
                                                            relatedBy:NSLayoutRelationEqual
                                                               toItem:_scrollView
                                                            attribute:NSLayoutAttributeLeading
                                                           multiplier:1.0f
                                                             constant:0]];
    [_scrollView addConstraint:[NSLayoutConstraint constraintWithItem:_contentView
                                                            attribute:NSLayoutAttributeTrailing
                                                            relatedBy:NSLayoutRelationEqual
                                                               toItem:_scrollView
                                                            attribute:NSLayoutAttributeTrailing
                                                           multiplier:1.0f
                                                             constant:0]];
    [_scrollView addConstraint:[NSLayoutConstraint constraintWithItem:_contentView
                                                            attribute:NSLayoutAttributeTop
                                                            relatedBy:NSLayoutRelationEqual
                                                               toItem:_scrollView
                                                            attribute:NSLayoutAttributeTop
                                                           multiplier:1.0f
                                                             constant:0]];
    [_scrollView addConstraint:[NSLayoutConstraint constraintWithItem:_contentView
                                                            attribute:NSLayoutAttributeBottom
                                                            relatedBy:NSLayoutRelationEqual
                                                               toItem:_scrollView
                                                            attribute:NSLayoutAttributeBottom
                                                           multiplier:1.0f
                                                             constant:0]];

STVerticalScrollContentViewのimageViewに画像を、textLabelに長いテキストを設定。

    _contentView.imageView.image = [UIImage imageNamed:@"image-300"];
    _contentView.textLabel.text = @"Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.";
}

画面表示される時や画面回転した後に、viewDidLayoutSubviewsが呼ばれる。この時点でUIScrollViewの幅が決定しているので、STVerticalScrollContentView#setLayoutWidthにUIScrollViewの幅を渡すと良い。

- (void)viewDidLayoutSubviews
{
    [_contentView setLayoutWidth:_scrollView.frame.size.width];
}

以上で正しく動くはずです。もう少し良い方法があるかもしれませんが、とりあえず今のところ自分はこの方法が安全確実だと思っています。

まとめ

最後にポイントをまとめておきます。

  • スクロールの中身のViewのレイアウトはxibファイルで行う
  • スクロールの中身のViewのクラスを作り、intrinsicContentSizeが適切なサイズを返すようにする
  • サイズ計算はsystemLayoutSizeFittingSizeを使うと便利
  • ViewController#viewDidLayouSubviewsを経由して、現在の幅に応じてスクロールの中身のViewのサイズを再計算
  • 再計算したらinvalidateIntrinsicContentSizeを呼ぶ