Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UICollectionViewController flow layout with readable content width

I have a UICollectionView using flow layout, and I'm trying to achieve the same margins as a UITableViewController with readable content width.

The closest I've come to matching this layout behavior is to embed a UICollectionViewController within a UIViewController and have the embedded view "Follow Readable Width".

UICollectionViewController embedded in UIViewController

Here the teal color is the UIViewController and the salmon color is the UICollectionViewController. The problem is the teal area doesn't scroll the UICollectionView and the scroll indicators are also not along the edge of the screen like you would expect.

My question is:

How can I achieve this layout without having to embed a UICollectionViewController?

My guess is that I can somehow set the UICollectionView left and right section insets to match the readable content guide margins and update them by overriding viewWillTransition(to size: with coordinator:) and observing UIContentSizeCategoryDidChange notifications, but I'm not sure how to go about this.

like image 415
Brandon Nott Avatar asked Mar 17 '26 15:03

Brandon Nott


2 Answers

In order to solve your problem with Swift 5.1 and iOS 13, you can set your flow layout's sectionInsetReference property to .fromContentInset, set your collection view's insetsLayoutMarginsFromSafeArea property to false and set your collection view's contentInsets to match the view's readableContentGuide's insets.


The following sample code shows a possible implementation in order to have a collection view that has a readable content width:

ViewController.swift

import UIKit

class ViewController: UIViewController {

    let collectionView = UICollectionView(
        frame: .zero,
        collectionViewLayout: ColumnFlowLayout(cellsPerRow: 5)
    )
    var collectionViewNeedsLayout = true

    override func viewDidLoad() {
        super.viewDidLoad()

        title = "Demo"

        view.addSubview(collectionView)
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])

        collectionView.backgroundColor = .white
        collectionView.insetsLayoutMarginsFromSafeArea = false
        collectionView.dataSource = self
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
    }

    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()

        if collectionViewNeedsLayout {
            let margin = view.readableContentGuide.layoutFrame.origin.x
            collectionView.contentInset.left = margin
            collectionView.contentInset.right = margin
            collectionViewNeedsLayout = false
        }
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        collectionViewNeedsLayout = true
    }

}
extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 59
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
        return cell
    }

}

ColumnFlowLayout.swift

import UIKit

class ColumnFlowLayout: UICollectionViewFlowLayout {

    let cellsPerRow: Int

    init(cellsPerRow: Int) {
        self.cellsPerRow = cellsPerRow

        super.init()

        self.sectionInsetReference = .fromContentInset
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func prepare() {
        super.prepare()

        guard let collectionView = collectionView else { return }

        let marginsAndInsets = collectionView.contentInset.left + collectionView.contentInset.right + sectionInset.left + sectionInset.right + minimumInteritemSpacing * CGFloat(cellsPerRow - 1)
        let itemWidth = ((collectionView.bounds.width - marginsAndInsets) / CGFloat(cellsPerRow)).rounded(.down)
        itemSize = CGSize(width: itemWidth, height: itemWidth)
    }

}

Cell.swift

import UIKit

class Cell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        contentView.backgroundColor = .orange
    }

    required init?(coder: NSCoder) {
        fatalError("not implemnted")
    }

}

Display on iPhone 11 Pro Max:

enter image description here

like image 165
Imanou Petit Avatar answered Mar 20 '26 06:03

Imanou Petit


On iOS 14+ with a diffable data source, you can use section.contentInsetsReference = .readableContent.

This will adjusts the section insets and follows the readable guide.

like image 29
Benoit Deldicque Avatar answered Mar 20 '26 07:03

Benoit Deldicque



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!