今回はUIScrollViewとAuto Layoutの組み合わせについてです。ここでハマっている人は多いのではないでしょうか。私も見事にハマリました。その経験から、きちんと動作させる方法を解説したいと思います。

※ 文中のIBはInterface Builderの略です。

以下のリンクのAppleのTechnical NoteにUIScrollViewとAuto Layoutの併用について書かれています。

UIScrollView and Autolayout

Mixed Approach(非Auto LayoutとAuto Layoutの組み合わせ)とPure Auto Layout Approach(Auto Layoutのみ)の2種類説明されていますが、後者に絞りたいと思います。スクロールの中身のサイズが可変する時も含めると、Pure Auto Layoutで対処したほうが確実で楽だと思います。

以下にPure Auto Layoutの説明を元にして作成したサンプルコードをコミットしてあります。

https://github.com/stack3/iOSAutoLayoutSamples

STLargeImageScrollViewControllerで、画面サイズを超える大きな画像を表示し、スクロールさせる画面を実装しています。

STLargeImageScrollViewController.storyboard

UIScrollViewはIBでViewControllerに配置しています。

ss04

Constraintは以下のとおり。画面全体に表示されるようになっています。

ss05

STLargeImageScrollViewController.m

viewDidLoadでスクロールする画像を表示するUIImageViewを生成し、ScrollViewに追加しています。

// 画面に収まらないくらいの大きな画像を読み込む
UIImage *image = [UIImage imageNamed:@"large-image-01"];
// 画像を元にUIImageViewを作成
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
// autoresizingMaskを自動的にConstraintに変換しない。
// これはaddSubviewする前にすること。
imageView.translatesAutoresizingMaskIntoConstraints = NO;
// ScrollViewのSubviewとして追加
[_scrollView addSubview:imageView];
// UIImageViewのConstraintを設定。上下左右の余白を0pxとする。
NSDictionary *viewDictionary = NSDictionaryOfVariableBindings(imageView);
[_scrollView addConstraints:
  [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[imageView]|"
                                          options:0
                                          metrics:nil
                                             views:viewDictionary]];
[_scrollView addConstraints:
  [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView]|"
                                          options:0
                                          metrics:nil
                                            views:viewDictionary]];

スクロールするView(この場合、UIImageView)のConstraintを、上下左右の空白を0にすることが大事です。こうすると、UIScrollView#contentSizeは自動的に、スクロールするViewのサイズに設定されます。contentSizeを自分で設定する必要はありません。さらに言うと自分で設定しても、自動的に書き換わってしまう可能性があります。Auto Layoutを使う場合、contentSizeは自分で設定しないほうが良いです。

ちょっとこの仕組みは不可解な印象もあります。このConstraintだとUIImageViewはUIScrollViewのサイズと同じになるだけのように思えます。ですが、実際はcontentSizeが更新され、正常に動作します。本家のAppleのTechnical Noteに書いてある内容なので信頼できるはずです。

動作確認

サンプルを起動して、Large Image ScrollViewを選択すると以下のように画像がスクロールします。

ここまでは簡単です。しかし、実際UIScrollViewを使う場合の多くは、いろいろな項目が縦に並び、縦方向にスクロールする画面だと思います。またそれらの項目は、IB + Auto Layoutで配置することになるでしょう。さらに複数行文字列などによる高さ可変、横画面による横幅可変という場合もあります。こういった画面を実現するためには、きちんとした理解が必要です。

Interface BuilderでスクロールするViewを配置できないか

IB上でUIImageViewをUIScrollViewのSubviewとして配置して、UIImageViewのConstraintを先に述べたのと同じになるようにIB上で設定します。そうすると以下のエラーと警告が表示されます。

ss03

Scrollable Content Size〜のエラーは、スクロールする内容となるViewのサイズがわからないということです。Misplaced Views〜の警告は、このViewは予期される幅と高さは0なのにそうなってないということです。

起因となっている問題は、UIImageViewのサイズがIBから判断できないことです。UIImageViewのサイズは、

  • imageプロパティで設定された画像のサイズ
  • もしくは、それ自身のWidth/HeightのConstraint

によって決定するからです。

IB上でimageプロパティを設定するか、Width/Heightのプロパティを設定すると、このエラーと警告は解決できます。しかし、固定の画像を表示する場合はそれで良いのですが、実際は異なるサイズの画像表示に対応する必要があるでしょう。また画像にかぎらずスクロールするViewのサイズが内容によって可変ということもよくあることです。

よってIB上だけでスクロールする画面を作るのは難しいと思います。

Visual Format Languageを使わない方法

少し話は戻って先ほどはVisual Format Languageを使ってConstraintを設定していました。Visual Format Languageを使うことを避けたい人もいると思います。自分も変数を文字列で書くことに対する抵抗があります。以下についてその辺を言及しています。

プログラムによるAuto Layoutをおすすめしない理由

今回はプログラムで書くことは避けられないですが、同様のことをVisual Format Language無しで書く方法も説明しておきます。

[_scrollView addConstraint:
  [NSLayoutConstraint constraintWithItem:imageView
                               attribute:NSLayoutAttributeLeading
                               relatedBy:NSLayoutRelationEqual
                                  toItem:_scrollView
                               attribute:NSLayoutAttributeLeading
                              multiplier:1.0f
                                constant:0]];
[_scrollView addConstraint:
  [NSLayoutConstraint constraintWithItem:imageView
                               attribute:NSLayoutAttributeTrailing
                               relatedBy:NSLayoutRelationEqual
                                  toItem:_scrollView
                               attribute:NSLayoutAttributeTrailing
                              multiplier:1.0f
                                constant:0]];
[_scrollView addConstraint:
  [NSLayoutConstraint constraintWithItem:imageView
                               attribute:NSLayoutAttributeTop
                               relatedBy:NSLayoutRelationEqual
                                  toItem:_scrollView
                               attribute:NSLayoutAttributeTop
                              multiplier:1.0f
                                constant:0]];
[_scrollView addConstraint:
  [NSLayoutConstraint constraintWithItem:imageView
                               attribute:NSLayoutAttributeBottom
                               relatedBy:NSLayoutRelationEqual
                                  toItem:_scrollView
                               attribute:NSLayoutAttributeBottom
                              multiplier:1.0f
                                constant:0]];

その2へつづく