This article is published on my wechat official account. You can follow it by scanning the QR code at the bottom of the article or searching for HelloWorld Jieshao in wechat.

Since Apple introduced the UICollectionView control in iOS6, more and more iOS developers have chosen it as the first choice for building uIs. What makes it so attractive is that it is highly customizable and flexible, depending on having a separate object to manage the layout. The layout determines the position and properties of the view.

Said to the layout of the layout, we in the development process and should use the most is the UICollectionViewFlowLayout UICollectionView collocation, this is the most basic UIKit provides developers the grid layout, If we ask for a slightly more customized layout, it will not meet the actual requirements. Can we implement a custom layout? The answer is yes, of course.

In today’s article, I’ll show you how to implement a custom waterfall flow layout, similar to the following:

In this process, you will learn the following knowledge points:

  1. About custom layouts
  2. Dynamic size Cell processing
  3. Compute and cache layout properties

Okay, no more nonsense, let’s get started!

Custom layout

Daily development, we use UICollectionView controls will match a default, provide some basic layout UICollectionViewFlowLayout to use, but when we need to implement interfaces customization degree is higher, have to implement a custom layout.

So, how can we implement a custom layout?

The layout of the UICollectionView is a subclass of the abstract UICollectionViewLayout class, which defines the layout property of each Item in the UICollectionView called: UICollectionViewLayoutAttributes, so we can through inheritance UICollectionViewLayout and UICollectionViewLayoutAttributes do adjustment for each item, Such as its size, rotation Angle, scaling and so on.

Now that Apple’s development documentation is clear, we can start with the basics:

  1. Create an inherited from UICollectionViewFlowLayout class WaterFallFlowLayout
  2. Declare a variable that represents the number of columns in the layout: cols
  3. Declare an array variable is used to cache computing good layout attributes: [UICollectionViewLayoutAttributes]
  4. Declare an array variable to hold the height of each column: [CGFloat]

The dynamic size

Some people ask, the amazing thing about waterfall flow view is that the size of each Cell is not consistent, how to generate dynamic height Cell!

In this case, I used Swift to generate a random number. When I set a frame for each item, I randomly generate a height. This is also the way we usually create dynamic interfaces.

CGFloat(arc4random_uniform(150) + 50)

Compute and cache layout properties

Before implementing this function, let’s take a look at the layout process of the UICollectionView. The relationship between the UICollectionView and the layout object is a cooperative relationship. When the UICollectionView needs some layout information, it will call some functions of the layout object. These functions are executed in a certain order, as shown in the figure below:

So our subclass from UICollectionViewLayout must implement the following methods:

1. override var collectionViewContentSize: CGSize {... }Copy the code

This method returns the width and height of the collection view’s contents. You must implement it to return the height And width of the entire collection view’s content, Not just the visible content. The collection view uses this information exchanges to configure its scroll view’s content size.

2. override func prepare() {... }Copy the code

Whenever a layout operation is about to take place, UIKit calls this method. It’s your opportunity to prepare and perform any calculations required to determine the collection view’s size and the positions of the items.

3. override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {... }Copy the code

In this method, you return the layout attributes for all items inside the given rectangle. You return the attributes to the collection view as an array of UICollectionViewLayoutAttributes.

4. override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {... }Copy the code

This method provides on demand layout information to the collection view. You need to override it and return the layout attributes for the item at the requested indexPath.

After understanding the need to implement the function, then began to calculate the waterfall flow view layout properties, here I first talk about the general idea of my implementation!

The Cell is highly dynamic

Since the height of each Cell in our waterfall flow view is dynamic, we can implement this requirement by declaring a protocol and providing a method that returns the dynamic height to provide the dynamic height for each Cell. The code is as follows:

protocol WaterFallLayoutDelegate: NSObjectProtocol {
    func waterFlowLayout(_ waterFlowLayout: WaterFallFlowLayout, itemHeight indexPath: IndexPath) -> CGFloat
}
Copy the code
Attribute evaluation

The highly dynamic Cell has been solved, so how can we make every Cell close together? My strategy here is to track and calculate the height of each column to find the column with the minimum height. Since we know the height and index of the column with the minimum height, we can calculate the new X and Y coordinates for a Cell, and then reassign the Cell’s position information. Finally, update the height of each column until the position has been recalculated for each Cell.

We can add this logic to the prepare() function as follows:

Override func prepare() {super.prepare(); let itemWidth = (collectionView! .bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(cols - 1)) / CGFloat(cols) // Let itemCount = collectionView! . NumberOfItems (inSection: 0) / / var minHeightIndex minimum height index = 0 / / traverse cache attribute for the item and I in layoutAttributeArray. Count.. < itemCount { let indexPath = IndexPath(item: i, section: 0) let attr = UICollectionViewLayoutAttributes(forCellWith: IndexPath) let itemHeight = delegate? .waterFlowLayout(self, itemHeight: Let value = yarray.min () minHeightIndex = yarray.firstIndex (of: value!) ! Var itemY = yArray[minHeightIndex] var itemY = yArray[minHeightIndex] If I >= cols {itemY += minimumInteritemSpacing} let itemX = sectionInset.left + (itemWidth) Attr. Frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: CGFloat(itemHeight!) ) / / cache layout attribute layoutAttributeArray. Append (attr) / / update the shortest height data column yArray [minHeightIndex] = attr. Frame. MaxY} maxHeight = yArray.max()! + sectionInset.bottom }Copy the code

Next, in layoutAttributesForElements (in the rect: CGRect) method to add the following logic:

This method determines which items are visible within a given area. We can use the filter method provided by the array to check whether the previously calculated layout properties intersect with the visible area and return the intersecting properties as follows:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    return layoutAttributeArray.filter {
        $0.frame.intersects(rect)
    }
}
Copy the code

Well, here is the layout of the waterfall flow view on the end, attached to the entire code of WaterFallFlowLayout, for your reference:

import UIKit protocol WaterFallLayoutDelegate: NSObjectProtocol { func waterFlowLayout(_ waterFlowLayout: WaterFallFlowLayout, itemHeight indexPath: IndexPath) -> CGFloat } class WaterFallFlowLayout: UICollectionViewFlowLayout { weak var delegate: WaterFallLayoutDelegate? Fileprivate lazy var layoutAttributeArray: [UICollectionViewLayoutAttributes] = [] / / height Array fileprivate lazy var yArray: [CGFloat] = Array (repeating: self.sectionInset.top, count: cols) fileprivate var maxHeight: CGFloat = 0 override func prepare() {super.prepare() let itemWidth = (collectionView! .bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(cols - 1)) / CGFloat(cols) // Let itemCount = collectionView! . NumberOfItems (inSection: 0) / / var minHeightIndex minimum height index = 0 / / traverse cache attribute for the item and I in layoutAttributeArray. Count.. < itemCount { let indexPath = IndexPath(item: i, section: 0) let attr = UICollectionViewLayoutAttributes(forCellWith: IndexPath) let itemHeight = delegate? .waterFlowLayout(self, itemHeight: Let value = yarray.min () minHeightIndex = yarray.firstIndex (of: value!) ! Var itemY = yArray[minHeightIndex] var itemY = yArray[minHeightIndex] If I >= cols {itemY += minimumInteritemSpacing} let itemX = sectionInset.left + (itemWidth) Attr. Frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: CGFloat(itemHeight!) ) / / cache layout attribute layoutAttributeArray. Append (attr) / / update the shortest height data column yArray [minHeightIndex] = attr. Frame. MaxY} maxHeight = yArray.max()! + sectionInset.bottom } } extension WaterFallFlowLayout { override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return layoutAttributeArray.filter { $0.frame.intersects(rect) } } override var collectionViewContentSize: CGSize { return CGSize(width: collectionView! .bounds.width, height: maxHeight) } }Copy the code

It’s presented in UIViewController

After completing the waterfall flow layout above, it is time to present it in UIViewController. The next steps are quite simple, and I will not explain them in detail.

import UIKit class WaterFallViewController: UIViewController { private let cellID = "baseCellID" var itemCount: Int = 30 var collectionView: UICollectionView! override func viewDidLoad() { super.viewDidLoad() // Do any additional setup after loading the view. setUpView() } func SetUpView () {// Set flowLayout let Layout = WaterFallFlowLayout() Layout.delegate = self // Set CollectionView let margin: CGFloat = 8 layout.minimumLineSpacing = margin layout.minimumInteritemSpacing = margin layout.sectionInset = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: Layout) collectionView. BackgroundColor =. White collectionView. The dataSource = self/Cell/registration collectionView.register(BaseCollectionViewCell.self, forCellWithReuseIdentifier: cellID) view.addSubview(collectionView) } } extension WaterFallViewController: UICollectionViewDelegate{ } extension WaterFallViewController: UICollectionViewDataSource{ func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return itemCount } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! BaseCollectionViewCell cell.cellIndex = indexPath.item cell.backgroundColor = indexPath.item % 2 == 0 ? .systemBlue : .purple if itemCount - 1 == indexPath.item { itemCount += 20 collectionView.reloadData() } return cell } } extension WaterFallViewController: WaterFallLayoutDelegate{ func waterFlowLayout(_ waterFlowLayout: WaterFallFlowLayout, itemHeight indexPath: IndexPath) -> CGFloat { return CGFloat(arc4random_uniform(150) + 50) } }Copy the code

Add the above code to the Xcode project to compile and run, and you’ll see that the cells are properly placed and sized according to the height of the photo:

Ok, use UICollectionView control and custom layout to achieve waterfall flow content to this end, finally attached to the project source address:

Github.com/ShenJieSuzh…

Related reading:

Use UICollectionView to achieve the paging slide effect

Use UICollectionView to achieve the card rotation effect of home page

Follow my technical public account “HelloWorld Jieshao “to get more quality technical articles.