環境
- Swift 5.4
- Xcode 12.5
概要
ReadableContentGuide
(以下RCG
) は、端末によって
コンテンツの読みやすい幅を実現するために役立つ UILayoutGuide
です。
Apple Developer ドキュメント - readableContentGuide
デザインによるコンテンツのマージン指定が特になければ、
UIDevice.current.userInterfaceIdiom
を判定して
iPhoneとiPadそれぞれに制約を設けることをせず、
RCGを利用して良い感じにマージンを設定することができます。
UITableViewやUIScrollViewで利用している例はちょこちょこ見かける気がするので、
今回はUICollectionViewの画面でRCGを利用した例を紹介します。
※注意
Storyboradは利用せず、コードでの実装です。
実装
UICollectionViewに対してRCGを適用する
UICollectionViewの制約をかける際に、
親Viewの SafeAreaLayoutGuide
ではなくRCGを基準とします。
そのためitemSizeを算出する時は、RCGの layoutFrame.width
を利用します。
コンテンツ上下左右のマージンはRCGに任せた実装になります。
import UIKit
final class ReadableCollectionViewController: UIViewController {
private let collectionView = UICollectionView(frame: .zero,
collectionViewLayout: UICollectionViewFlowLayout())
private let itemSpacing: CGFloat = 10
override func loadView() {
super.loadView()
view.backgroundColor = .systemBackground
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.backgroundColor = .secondarySystemBackground
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.readableContentGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.readableContentGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor)
])
}
}
extension ReadableCollectionViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
30 // 任意の数
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)
cell.backgroundColor = .gray
return cell
}
}
extension ReadableCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let cellSideLength: CGFloat = (view.readableContentGuide.layoutFrame.width - itemSpacing * 2) / 3
return .init(width: cellSideLength, height: cellSideLength)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
.zero
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
itemSpacing
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
itemSpacing
}
}
画面キャプチャ
インジケータがコンテンツと被ってしまっているのでinsetの設定はあった方が良さそうです。
また、CollectionViewと親Viewの背景色が異なる場合は注意が必要です。
iPhone8 | iPadPro(12.9-inch) |
---|---|
UICollectionViewのセクションに対してRCGを適用する
先の実装とは異なり、UICollectionViewの制約は、
親Viewの SafeAreaLayoutGuide
を基準とします。
その代わりにSectionのInsetを設定する箇所で
計算処理を追加しています。
※ 先の実装と変更がない箇所は処理を省略しています
import UIKit
final class ReadableCollectionViewController: UIViewController {
// 先の実装と同様
override func viewDidLoad() {
super.viewDidLoad()
// 先の実装と同様
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
collectionView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor)
])
}
}
extension ReadableCollectionViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// 先の実装と同様
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// 先の実装と同様
}
}
extension ReadableCollectionViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
// 先の実装と同様
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
let topOrBottomInset: CGFloat = (view.frame.height - view.readableContentGuide.layoutFrame.height) / 2
let leftOrRightInset: CGFloat = (view.frame.width - view.readableContentGuide.layoutFrame.width) / 2
return .init(top: topOrBottomInset, left: leftOrRightInset,
bottom: topOrBottomInset, right: leftOrRightInset)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
// 先の実装と同様
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
// 先の実装と同様
}
}
画面キャプチャ
CollectionViewが画面全体に広がっているのでインジケータは見やすくなりました。
Sectionが複数になった時にtopとbottomのマージン設定を改善しないといけなさそうです。
iPhone8 | iPadPro(12.9-inch) |
---|---|
後記
今回実装した画面は動的にitemのサイズが切り替わらないので
UICollectionViewFlowLayout
のインスタンスをCollectionViewに渡して実現しようとしましたが、
viewWillAppear(_:)
まではRCGのlayoutFrameの正確な値を
取得できなかったため、Delegateを利用しています。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("viewWillAppear:\(view.readableContentGuide.layoutFrame)")
}
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
print("viewWillLayoutSubviews:\(view.readableContentGuide.layoutFrame)")
}
// iPhone8の場合
// viewWillAppear:(0.0, 0.0, 375.0, 667.0)
// viewWillLayoutSubviews:(16.0, 20.0, 343.0, 647.0)