Key UICollectionView Concepts
Layout
Data source
Delegate
Layout
allows
UICollectionViewto abstract away the visual arrangement of your content from the content itselfis all about the where content is displayed
supports invalidation for when, for example, you want to change the appearance of the layout
can animate between layouts (without the need for layouts knowing anything about other layouts)
UICollectionViewLayoutis an abstract class not meant to be used directly. Subclasses ofUICollectionViewLayout, such asUICollectionViewFlowLayout, are meant to be used
Each individual item is specified by UICollectionViewLayoutAttributes:
contains attributes for visual arrangement such as bounds, center, and frame
you can think of it as a set of properties you can use to define these items that are displayed
UICollectionViewFlowLayout
line-based layout covers a wide range of designs
line spacing: space between lines (you can set the min space via
minimumLineSpacing)inter-item spacing: space within items in the same line (you can set the min space via
minimumInteritemSpacing)
Data source
Layout is all about the where content goes, the data source is the what
UICollectionViewDataSource has three core methods:
/// Defaults to one section when not implemented
optional func numberOfSections(in collectionView: UICollectionView) -> Int
/// Number of items for the given section
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
/// Actual content you're going to display to your users
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCellDelegate
Optional to implement
Extends
UIScrollViewDelegateFine-grained control over:
Highlighting
Selection
View appearance events
willDisplayItemdidEndDisplayingItem
UICollectionViewLayout
prepare() is called whenever the layout is invalidated. In the case of UICollectionFlowLayout, our layout is invalidated whenever the UICollectionView’s bounds’s size changes. This is a great place to do any customization that takes the size of the UICollectionView into account.
In the following example we use prepare() to update the itemSize and sections insets based on the collectionView.bounds:
class ColumnFlowLayout: UICollectionViewFlowLayout {
private let minColumnWidth: CGFloat = 300.0
private let cellHeight: CGFloat = 70.0
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let availableWidth = collectionView.bounds.inset(by: collectionView.layoutMargins).width
let maxNumColumns = Int(availableWidth / minColumnWidth)
let cellWidth = (availableWidth / CGFloat(maxNumColumns)).rounded(.down)
self.itemSize = CGSize(width: cellWidth, height: cellHeight)
self.sectionInset = UIEdgeInsets(top: self.minimumInteritemSpacing, left: 0.0, bottom: 0.0, right: 0.0)
self.sectionInsetReference = .fromSafeArea
}
...
}Creating a Custom UICollectionViewLayout
We should try to stick with UICollectionFlowLayout whenever possible, however, if our layout is not line-based, it’s completely acceptable to subclass UICollectionViewLayout.
Four basic methods
1. Providing Content Size
open var collectionViewContentSize: CGSize { get }Size of bounds which contains all items - a.k.a., if you imagine a rectangle that encompassed all the content that the layout is going to define for your
UICollectionView,UICollectionViewLayoutneeds to return the size of that.Needed for
UIScrollView.contentSize
2. & 3. Providing Layout Attributes
/// Called periodically by `UICollectionView` when it needs to know what is needed to display
/// on screen as the user scrolls through your content, or displays for the first time.
///
/// In other words `UICollectionView` is asking for a set of attributes that match a certain
/// region. It's our job to return an array that contains all the attributes that correspond to all
/// the items that are going to appear within that rect in our `UICollectionView`.
func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
/// Asks for the `UICollectionViewLayoutAttributes` of the given item.
func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?Query by geometric region
Query by IndexPath
Performance matters
4. Preparing the Layout
func prepare()Called for every
invalidateLayout()(just once per invalidation)This is the time for you to:
cache
UICollectionViewLayoutAttributescompute
collectionViewContentSize
class CustomLayout: UICollectionViewLayout {
// Cached information
var contentBounds: CGRect = .zero
var cachedAttributes: [UICollectionViewLayoutAttributes] = []
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
// Reset cached information.
cachedAttributes.removeAll()
contentBounds = CGRect(origin: .zero, size: collectionView.bounds.size)
// For every item in the collection view:
// - Prepare the attributes.
// - Store attributes in the cachedAttributes array.
// - Combine contentBounds with attributes.frame.
...
}
}
}5. (bonus) Handling Bounds Changes in Your Custom Layout
func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> BoolCalled for every
boundschange (bothsizeand/ororigin)Called during scrolling (as the origin changes)
Default implementation returns
falseUICollectionFlowLayoutreturnstrueonly whenbounds.sizechanges
performBatchUpdates(_:completion:)
performBatchUpdates(_:completion:):
Animate updates together
Perform data source updates and collection view updates in updates closure
Collection view updates ordering does not matter within the block
data source updates ordering does matter
Collection View Updates Coalescing
| Action | Notes | Index Path Semantics |
|---|---|---|
| Delete | Descending IndexPath order | Before updates |
| Insert | Ascending IndexPath order | After updates |
| Move | From: Before updates. To: After updates. | |
| Reload | Decompose: Delete and inserts | Before updates |
Exceptions will result from:
Move and Delete the same location
Move and Insert to the same location
Move more than 1 location to the same location
Referencing an invalid IndexPath
How to use performBatchUpdates(_:completion:) safely:
Decompose Move into Delete and Insert updates
Combine all Delete and Insert updates
Process Delete updates first, in descending order
Process Insert updates last, in ascending order
