A Day In The Life

とあるプログラマの備忘録

Autolayout に対応した画面で UIScrollView を使う時のコツ

iOS 6から導入された Autolayout(オートレイアウト)を最近になって本格的に使うようになりました。
4-inch の iPhone が主流になりつつある中、アプリを Autolayout に対応させるのはほぼ必須といっても良いと思います。Storyboard と格闘してなんとかコツはつかめたものの UIScrollView だけは一筋縄ではいかず苦労しました。
というわけで Autolayout に対応した画面で UIScrollView を使う時のコツをまとめてみました。
なおこの記事で説明する内容は Storyboard 上で Autolayout がオンになっていることが前提になっています。
Autolayoutをオンにする

Autolayout 対応画面で UIScrollView を使うための2種類の方法

Autolayout に対応した画面で UIScrollView を使うには以下の2つの方法があります。

  • Mixed Approach
    Autolayout と AutoresizingMask(iOS 6以前からあるレイアウト方法)を組み合わせる方法
  • Pure Auto Layout Approach
    Autolayout だけを使う方法

どちらのアプローチで実装するにしても UIScrollView オブジェクトは Autolayout を使って配置します。違いは UIScrollView オブジェクトの中に配置するビュー(Content View といいます)を AutoresizingMask を使って配置するか Autolayout を使って配置するかです。
Mixed Approach と Pure Auto Layout Approach に関する詳細は以下のアップル公式ドキュメントを参考にしてください。

サンプルアプリの概要

以下のサンプルアプリを実装しながら説明していきます。
アプリのイメージ
画面の中央に 320 × 170 サイズの UIScrollView オブジェクトを配置して、その中に UIView オブジェクトを2つ横に並べています。表示範囲を横スクロールさせるアプリです。

前準備

それぞれの実装方法を説明する前に準備として UIScrollView オブジェクトを配置しておきます。

UIScrollView オブジェクトの配置

Storyboard を使って、320 * 170サイズの UIScrollView オブジェクトを配置します。
UIScrollView を配置する

Constraints を追加

配置した UIScrollView オブジェクトを選択した状態で Constraints を追加します。まずはじめに UIScrollView オブジェクトの大きさを固定します。
大きさを固定する
次に UIScrollView オブジェクトを画面の真ん中にくるように設定します。
真ん中に設定する

プロパティを追加して IBOutlet 接続する

以下のように UIViewController の子クラス(ここでは仮に ViewController クラスとします)に UIScrollView 型のプロパティを追加します。

@interface ViewController : UIViewController

@property (nonatomic, strong) IBOutlet UIScrollView *scrollView;

@end

Storyboard で追加したプロパティを IBOutlet 接続します。
以上で前準備は完了です。それでは両アプローチの実装方法を見ていきましょう。

Mixed Approach を使って実装する

Autolayout と AutoresizingMaskを組み合わせる方法です。UIScrollView オブジェクトを Autolayout で配置して、スクロールビューの中に配置するビューを Autolayout を使わずに配置します。
以下のように viewDidLoad メソッドを実装します。

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];

  /*
   * ナビゲーションコントローラを使って
   * ビューコントローラを管理している場合は下記設定が必要
   */
  self.automaticallyAdjustsScrollViewInsets = NO;

  // 1つ目のビューのインスタンス生成
  UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(40, 0, 240, 170)];

  // 2つ目のビューのインスタンス生成
  UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(300, 0, 240, 170)];

  // スクロールビューにビューを追加する
  [self.scrollView addSubview:view1];
  [self.scrollView addSubview:view2];

  // スクロールビューのコンテンツサイズを設定する
  self.scrollView.contentSize = CGSizeMake(580, 170);
}

@end

UIScrollView オブジェクトにコンテントビュー(view1 と view2)を配置して contentSize を設定するおなじみ方法です。注意点としてはコメントにも書いた通り、ビューコントローラがナビゲーションコントローラーに管理されている場合、ビューコントローラの automaticallyAdjustsScrollViewInsets プロパティに NO を設定しないとコンテントビューの位置がずれてしまうことです(モーダルビューの場合はこの設定は不要です)。どうやら iOS 7 特有の問題のようです。詳しくはこちらの記事を参照してください。

Pure Auto Layout Approach を使って実装する

純粋に Autolayout だけを使って実装する方法です。Mixed Approach に比べると少しコードが長くなる傾向にあります。Autolayout は出来る限り Storyboard を使って設定したい所ではありますが UIScrollView に関して想定通りに動いたためしがないのでコードで書くことにします。
以下のように viewDidLoad メソッドを実装します。

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];
    
  /*
   * ナビゲーションコントローラを使って
   * ビューコントローラを管理している場合は下記設定が必要
   */
  self.automaticallyAdjustsScrollViewInsets = NO;
    
  // 1つ目のビューのインスタンス生成
  UIView *view1 = [[UIView alloc] init];
    
  // 2つ目のビューのインスタンス生成
  UIView *view2 = [[UIView alloc] init];
    
  // スクロールビューにビューを追加する
  [self.scrollView addSubview:view1];
  [self.scrollView addSubview:view2];
    
  // AutoresizingMaskをオフにする
  self.scrollView.translatesAutoresizingMaskIntoConstraints = NO;
  view1.translatesAutoresizingMaskIntoConstraints = NO;
  view2.translatesAutoresizingMaskIntoConstraints = NO;
    
  // Constraint(制約)を追加
  [self.scrollView addConstraints:
   [NSLayoutConstraint constraintsWithVisualFormat:@"|-40-[view1(==240)]-20-[view2(==240)]-40-|"
                                             options:0
                                             metrics:0
                                               views:NSDictionaryOfVariableBindings(view1, view2)]];
  [self.scrollView addConstraints:
   [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[view1(==170)]"
                                             options:0
                                             metrics:0
                                               views:NSDictionaryOfVariableBindings(view1)]];
  [self.scrollView addConstraints:
   [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-0-[view2(==170)]"
                                             options:0
                                             metrics:0
                                               views:NSDictionaryOfVariableBindings(view2)]];
}

@end

UIScrollView オブジェクトの contentSize はAutolayout によって自動で計算されるため設定する必要がありません。また UIScrollView オブジェクトとその中に配置するコンテントビューの translatesAutoresizingMaskIntoConstraints プロパティはすべて NO に設定する必要があります。

制約を記述する Visual Format Language

Autolayout ではビューの位置を Constraint(制約)を使って指定します。Constraint を指定するには NSLayoutConstraint の constraintsWithVisualFormat:options:metrics:views: メソッドを使います。このメソッドの第1引数には Visual Format Language と呼ばれる特殊な文字列を渡します。サンプルコード上の1つ目の Visual Format である |-40-[view1(==240)]-20-[view2(==240)]-40-| を図に表すと以下のようになります。
Visual Formatの意味
左端からマージンを40あけて1つ目のビューを240の幅で配置し、さらにマージンを20あけてから2つ目のビューを240の幅で配置、最後に右端から2つ目のビューまで40あけるという意味になります。
残り2つの Visual Format の V:|-0-[view1(==170)]V:|-0-[view2(==170)] は指定しているビューが違うだけで意味は同じです。図に表すと以下のようになります。
Visual Formatの意味その2
縦位置のマージンは0、ビューの高さは170という意味になります。「V」はおそらく Vertical の V です。

参考

Autolayout と UIScrollView についての詳細は以下の Apple 公式ドキュメントを参考にしてください。

UIViewController クラスの automaticallyAdjustsScrollViewInsets プロパティについては以下の記事を参考にしてください

Autolayout の基礎的な内容については以下の記事が参考になりました。

Autolayout と UIScrollView についての Stackoverflow の議論

サンプルソース

この記事で使用したサンプルプログラムをこちらに置いておきます。