A UICollectionView grid layout designed to support Dynamic Type
flexiblerowheightgridlayout
FlexibleRowHeightGridLayout is a UICollectionViewLayout which lays out self-sizing cells in a grid and is designed to support accessibility, in particular, Dynamic Type. It is designed to automatically re-layout with changes in text size on the device (UIContentSizeCategory). Row heights are flexible with this layout i.e. each row may have different height where the height of the row is determined by the tallest cell in the row so that the row height will always fit the content within the row.
Features
- [x] Grid layout supporting changes in text size - will automatically re-layout with changes in
UIContentSizeCategory
(Dynamic Type). - [x] Supports self-sizing UICollectionViewCells.
- [x] Supports sections including headers and / or footers.
Requirements
FlexibleRowHeightGridLayout is written in Swift 5.0 and is available on iOS 8.0 or higher.
Installation
Cocoapods
CocoaPods is a dependency manager which integrates dependencies into your Xcode workspace. To install it using Ruby gems run:
gem install cocoapods
To install FlexibleRowHeightGridLayout using Cocoapods, simply add the following line to your Podfile:
pod "FlexibleRowHeightGridLayout"
Then run the command:
pod install
For more information see here.
Carthage
Carthage is a dependency manager which produces a binary for manual integration into your project. It can be installed via Homebrew using the commands:
brew update
brew install carthage
In order to integrate FlexibleRowHeightGridLayout into your project via Carthage, add the following line to your project's Cartfile:
github "rwbutler/FlexibleRowHeightGridLayout"
From the macOS Terminal run carthage update --platform iOS
to build the framework then drag FlexibleRowHeightGridLayout.framework
into your Xcode project.
For more information see here.
Swift Package Manager
Xcode 11 includes support for Swift Package Manager. In order to add FlexibleRowHeightGridLayout to your project in Xcode 11, from the File
menu select Swift Packages
and then select Add Package Dependency
.
A dialogue will request the package repository URL which is:
https://github.com/rwbutler/FlexibleRowHeightGridLayout
After verifying the URL, Xcode will prompt you to select whether to pull a specific branch, commit or versioned release into your project.
Proceed to the next step by where you will be asked to select the package product to integrate into a target. There will be a single package product named FlexibleRowHeightGridLayout
which should be pre-selected. Ensure that your main app target is selected from the rightmost column of the dialog then click Finish to complete the integration.
Usage
In order to have your UICollectionView
make use of FlexibleRowHeightGridLayout
to layout content you may either instantiate a new instance and assign it to the collection view's collectionViewLayout
property if your collection view is defined in Interface Builder as follows:
let layout = FlexibleRowHeightGridLayout()
layout.delegate = self
collectionView.collectionViewLayout = layout
// Following line is only required if content has already been loaded before collectionViewLayout property set.
collectionView.reloadData()
Or, if your UICollectionView
is instantiated programmatically then you may pass the layout to the UICollectionView
's initializer:
let layout = FlexibleRowHeightGridLayout()
layout.delegate = self
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
Notice that in both cases you are required to provide an implementation of the layout's delegate -FlexibleRowHeightGridLayoutDelegate
.
FlexibleRowHeightGridLayoutDelegate
The delegate defines two methods which must be implemented in order to allow FlexibleRowHeightGridLayout
to layout items correctly. These are:
func collectionView(_ collectionView: UICollectionView, layout: FlexibleRowHeightGridLayout, heightForItemAt indexPath: IndexPath) -> CGFloat
Should return the height of the item for the given IndexPath
. Using this information the layout is able to calculate the correct height for the row. When calculating the height of text, you find it useful to make use of NSString
's func boundingRect(with size: options: attributes: context:) -> CGRect function as follows:
let constraintRect = CGSize(width: columnWidth, height: .greatestFiniteMagnitude)
let textHeight = "Some text".boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil).height
In order to help you determine the height of your content, FlexibleRowHeightGridLayout provides a couple of useful methods such as textHeight(_ text: String, font: UIFont)
to help you calculate the height required to display a String rendered using the specified font. If your cell were to contain a label pinned to each of the edges of the cell’s content view then the height of the cell’s content could easily be calculated as follows:
func collectionView(_ collectionView: UICollectionView, layout: FlexibleRowHeightGridLayout, heightForItemAt indexPath: IndexPath) -> CGFloat {
let text = dataSource.item(at: indexPath.item)
let font = UIFont.preferredFont(forTextStyle: .body)
return layout.textHeight(text, font: font)
}
These helper methods are particular useful for developers who are already using TypographyKit (a framework which also supports Dynamic Type by automatically updating the text size on UIKit elements when the users changes the text size preference on their device). Calculating the height of a cell containing a single label can be achieved as follows:
func collectionView(_ collectionView: UICollectionView, layout: FlexibleRowHeightGridLayout, heightForItemAt indexPath: IndexPath) -> CGFloat {
let text = dataSource.item(at: indexPath.item)
let font = Typography(for: .cellText).font()
return layout.textHeight(text, font: font)
}
Or if your cell is defined in a nib, then it is possible to inflate a cell in order to calculate the cell height as follows:
let text = dataSource.item(at: indexPath.item)
guard let nib = Bundle.main.loadNibNamed("CustomCell", owner: CustomCell.self, options: nil), let cell = nib?[0] as? CustomCell else {
return
}
// Ensure that your content has been set
cell.label.text = text
// Assuming your custom cell has a content view
cell.contentView.setNeedsLayout()
cell.contentView.layoutIfNeeded()
let size = cell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
return size.height
func numberOfColumns(for size: CGSize) -> Int
Should return the number of columns in the UICollectionView
's grid when the UICollectionView
is of the specified size. For example, the UICollectionView
may have larger bounds when the device is in landscape and therefore you may want your UICollectionView
to have 4 columns when the device is in landscape orientation and only 3 when in portrait.
There are another two delegate methods which may optionally be implemented should you wish to include a header and / or footer as part of your UICollectionView:
@objc optional func collectionView(_ collectionView: UICollectionView, layout: FlexibleRowHeightGridLayout, referenceHeightForHeaderInSection section: Int) -> CGFloat
Should return the height of the header in your UICollectionView. If the value returned from this function is zero than no header will be added.
@objc optional func collectionView(_ collectionView: UICollectionView, layout: FlexibleRowHeightGridLayout, referenceHeightForFooterInSection section: Int) -> CGFloat
Should return the height of the footer in your UICollectionView. If the value returned from this function is zero than no footer will be added.
FAQS
Does this layout support section headers and / or footers?
Yes, in order to add a section header and / or footer to your UICollectionView ensure that you provide an implementation for the two optional delegate methods in FlexibleRowHeightGridLayoutDelegate
:
-
@objc optional func collectionView(_ collectionView: UICollectionView, layout: FlexibleRowHeightGridLayout, referenceHeightForHeaderInSection section: Int) -> CGFloat
-
@objc optional func collectionView(_ collectionView: UICollectionView, layout: FlexibleRowHeightGridLayout, referenceHeightForFooterInSection section: Int) -> CGFloat