Help us understand the problem. What is going on with this article?

StackView in a ScrollView

どうも、iOSアプリ開発をしておりますRyu1です。
最近ふかみんというあだ名を付けてもらって少し気に入ってます。
今回は、UIScrollViewにUIStackViewを配置して、スクロールする方法を紹介します。

何故この記事を書くか

UIScrollViewにUIStackViewを配置してスクロールする方法を紹介している記事は既にいくつかあるものの、

  • Xcode11で追加されたframe and content layout guidesと合わせて書いてある記事が少ない
  • ScrollViewのContent sizeをわかりやすく解説している記事が少なかった。

などといった理由で、本記事を書くことにしました。

環境

  • Xcode Version 11.3.1
  • Swift5

実現したいこと

UIScrollViewにUIStackViewを重ねて、UIStackViewにUIViewを5つ程配置してスクロールする。

↓こんな感じ。
scroll.gif

必要となる知識

frame layout guides

Appleのドキュメントに以下のように記載されている通り、

Use this layout guide when you want to create Auto Layout constraints that explicitly involve the frame rectangle of the scroll view itself, as opposed to its content rectangle.

と書かれているので、ScrollView自体の形を指定する制約を設定する際に、このlayout guideを用います。

content layout guides

同様に、Appleのドキュメントに以下のように記載されている通り、

Use this layout guide when you want to create Auto Layout constraints related to the content area of a scroll view.

ScrollViewの中のContent area(今回で言うとStackView)に関する制約を設定する際に、このlayout guideを用います。

frame and content layout guides

上記の

  • frame layout guides
  • content layout guides

は、あくまでlayoutを組むのを手助けするものであり、必ずしも必要なものではありません。
実装の欄で制約の組み方を見ていただければ、これらがなくとも実装可能であることが確認できると思います。
これらを使うかどうかは、以下のチェック項目で選択できます。
スクリーンショット 2020-02-18 9.49.23.png

ScrollViewのcontent size

今回のように、ScrollViewのContent ViewをAuto layoutで組む場合は、Content sizeを設定してあげなければなりません。これは、Scrollable Areaの範囲を設定するためで、Scroll Viewの制約がなかなか定まらないのはこのためです。
Content SizeがScroll Viewのframe sizeよりも大きい場合のみScrollできます。

また、Auto layoutとCodeで組む場合で、それぞれContent sizeの設定の仕方が異なります。

Auto layoutの場合

以下の2点を設定する必要があります。
- Content View(今回でいうStack View)のwidthとheightの両方
- Content View(今回でいうStack View)の4辺をScroll Viewに合わせる

Codeの場合
scrollview.contentSize = CGSize(width: 640, height: 800)

intrinsic content sizeとは

intrinsicとは、固有のという意味で、intrinsic content sizeは、Viewのコンテンツを表示するための最低のサイズを表しています。
UIViewのプロパティであるintrinsicContentSizeは読み取り専用ですが、UIViewのサブクラスで、以下のように、intrinsicContentSizeをオーバーライドすることでintrinsicContentSizeを変更することもできます。

class ExampleView: UIView {

    public override func awakeFromNib() {
        super.awakeFromNib()
    }

    override var intrinsicContentSize: CGSize {
        CGSize(width: 170, height: 230)
    }

また、XibやStoryBoardでも以下のように、Intrinsic SizeをDefaultからPlaceholderにして設定することができます。またこれは、編集時のみ適応されるものであり、ランタイムには無視されるため、中身の大きさによってそのViewのサイズが変更される場合に、使い勝手がいいです。
スクリーンショット 2020-02-17 23.38.07.png

実装

0. ファイル生成

今回は画面繊維や細かいViewを使う必要はないので、
New file...Cocoa Touch ClassUIViewControllerを継承した.swiftファイルとAlso create XIB fileを選択して、xibファイルを生成。
つまり、今回主に扱うファイルは2つです。

1. xibでUIを組む。

1.1 UIScrollView

まずはScrollViewを配置します。
AppleのAuto Layout Guideにも以下のように記載されている通り、特別なことは何もせずに、通常通り制約を付けます。

Any constraints between the scroll view and objects outside the scroll view attach to the scroll view’s frame, just as with any other view.

以下のように制約を組みます。
スクリーンショット 2020-02-17 23.28.49.png
スクリーンショット 2020-02-17 23.29.23.png

ここで、ScorllViewの中にContent Layout GuideFrame Layout Guideというものに目が行くかと思いますが、一旦無視します。
また、この時点で制約が足りないと言われていますが、それも無視します。(これは、

1.2 UIStackView

まずは、UIStackViewをUIScrollViewの上に配置します。
Distributionなどの設定は以下の通りです。
スクリーンショット 2020-02-17 23.39.58.png

1.2.1 Frame Layout Guide

その後、上の説明で記載した通り、frame layout guidesを用いてScrollView自体の形に関する制約を付けます。
以下のように、StackViewFrame Layout Guideを同時に選択した状態で、Add New Constraintsを押し、Equal Heightsを設定します。
スクリーンショット 2020-02-17 23.58.38.png

1.2.2 Content Layout Guide

上の説明で記載した通り、content layout guidesを用いてStackViewに関する制約を付けます。
content.gif
上の操作によって、下のような制約を付けます。(多少値を修正する必要あり)
スクリーンショット 2020-02-18 0.58.55.png

1.2.3 Scroll Viewの横幅の設定

この状態では、以下のような赤文字が出てしまいます。
スクリーンショット 2020-02-17 23.35.39.png
Scroll Viewの横幅(つまり、その子であるStack Viewの横幅)に関する制約が不足しているそうです。ここで、StackViewの横幅を設定する必要があるのですが、注意が必要で、Scroll Viewの横幅を単純にSuper Viewとイコール関係で追加してしまっては、Priorityが、Stack Viewに配置するUIViewの大きさの設定のPriorityよりも下がらないままStackViewの横幅が設定されてしまいます。
その結果、

  • StackViewの横幅を縛ってしまうので、Scrollしない
  • UIViewの大きさがおかしくなる という事態が発生してしまいます。

よって、ここで設定すべきは、Stack Viewのintrinsic content sizeです。

1.2.4 intrinsic content sizeの設定

intrinsic content sizeの説明を見てみると、

Setting a design time intrinsic content size only affects a view while editing in Interface Builder. The view will not have this intrinsic content size at runtime.

と書かれてており、ここで設定したサイズは、IBの編集中にのみ適応されるもので、ランタイムはここでなされた設定は無視されます。
スクリーンショット 2020-02-17 23.38.07.png
横幅のみでいいので、HeightはNoneにチェックを入れます。Widthの値は何でも良いので、0を入れておきます。
これによって、StackViewの横幅を縛ることなく、StackViewに配置されたUIViewによってStack Viewの横幅を決めれることで、適切にスクロールさせることができます。

また、intrinsic content sizeを使わずに、Stack Viewのwidth >= 0というように設定し、priorityを下げる、という方法でもうまくいきますが、Stack ViewにStackするコンテンツによって上書きされてしまうので、無駄な制約をつけることになってしまうかつ、ランタイム中に使わない制約を付けてしまうという意味でもあまりいい手法とは言えません。よって、今回はintrinsic content sizeを使って、暫定的な幅を設定してあげるのが良さそうです。

3. IBとコードを繋げる

ここに関する説明は特にいらないと思うので、割愛します。

import UIKit

class ScrollStackViewController: UIViewController {

    @IBOutlet weak var stackView: UIStackView!

    override func viewDidLoad() {
        super.viewDidLoad()

        prepareForStackView()
    }

    // Stack Viewと同じのheightのUIViewを5つStack Viewに配置する
    private func prepareForStackView() {
        let stackedViewSize = stackView.bounds.height

        for _ in 1..<5 {
            let stackedView = UIView()
            stackedView.backgroundColor = .black
            stackedView.widthAnchor.constraint(equalToConstant: stackedViewSize).isActive = true
            stackedView.heightAnchor.constraint(equalToConstant: stackedViewSize).isActive = true
            stackView.addArrangedSubview(stackedView)
        }
    }
}

完成

以上の手順を踏むと、冒頭で掲載した通りの、Stack ViewをScrollするというViewを実装できます。
scroll.gif

参考文献

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした