LoginSignup
1
1

More than 1 year has passed since last update.

ReadableContentGuideを利用してUICollectionViewのコンテンツを見やすくする

Last updated at Posted at 2021-04-29

環境

  • 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)
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1