どうも、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つ程配置してスクロールする。
必要となる知識
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を組むのを手助けするものであり、必ずしも必要なものではありません。
実装の欄で制約の組み方を見ていただければ、これらがなくとも実装可能であることが確認できると思います。
これらを使うかどうかは、以下のチェック項目で選択できます。
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のサイズが変更される場合に、使い勝手がいいです。
実装
0. ファイル生成
今回は画面繊維や細かいViewを使う必要はないので、
New file...
→ Cocoa Touch Class
→ UIViewController
を継承した.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.
ここで、ScorllViewの中にContent Layout Guide
とFrame Layout Guide
というものに目が行くかと思いますが、一旦無視します。
また、この時点で制約が足りないと言われていますが、それも無視します。(これは、
1.2 UIStackView
まずは、UIStackViewをUIScrollViewの上に配置します。
Distributionなどの設定は以下の通りです。
1.2.1 Frame Layout Guide
その後、上の説明で記載した通り、frame layout guides
を用いてScrollView自体の形に関する制約を付けます。
以下のように、StackView
とFrame Layout Guide
を同時に選択した状態で、Add New Constraints
を押し、Equal Heights
を設定します。
1.2.2 Content Layout Guide
上の説明で記載した通り、content layout guides
を用いてStackViewに関する制約を付けます。
上の操作によって、下のような制約を付けます。(多少値を修正する必要あり)
1.2.3 Scroll Viewの横幅の設定
この状態では、以下のような赤文字が出てしまいます。
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の編集中にのみ適応されるもので、ランタイムはここでなされた設定は無視されます。
横幅のみでいいので、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を実装できます。
参考文献
- https://blog.alltheflow.com/scrollable-uistackview/
- https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithScrollViews.html#//apple_ref/doc/uid/TP40010853-CH24-SW1
- https://developer.apple.com/documentation/uikit/uiscrollview/2865870-contentlayoutguide
- https://riptutorial.com/ios/example/5109/scroll-view-content-size