UICollectionView came out in iOS6, and a lot of people compare it to UITableView, which is much more powerful and much more flexible than the layout structure of UITableView. Flexible layouts such as waterfall streams are implemented using UICollectionView. In this article, we will take a look at how UICollectionView achieves such a flexible layout.

UICollectionView is flexible because of UICollectionViewLayout, so what is UICollectionViewLayout?

UICollectionViewLayout profile

So what is UICollectionViewLayout? Let’s take a look at the official API explanation first:

The UICollectionViewLayout class is an abstract base class that you subclass and use to generate layout information for a collection view. The job of a layout object is to determine the placement of cells, supplementary views, and decoration views inside the collection view’s bounds and to report that information to the collection view when asked. The collection view then applies the provided layout information to the corresponding views so that they can be presented onscreen.

The UICollectionViewLayout class is an abstract class that uses the layout information that generates the UICollectionView. His job during the layout is to determine the location of the Cell Settings and other layout information. The layout information provided by UICollectionView is then applied to the corresponding views so that they can be rendered on the screen.

UICollectionViewLayout determines the layout of the CollectionView.

The use of UICollectionViewLayout

Now that we know that UICollectionView relies on UICollectionViewLayout, how can we use UICollectionViewLayout to create a waterfall layout?

You must first subclass UICollectionViewLayout and rewrite the UICollectionViewLayout method. Let’s write a waterfall stream as an example: take a look at the sample code

/ / / / / / YHYCollectionViewLayout swift mapps / / / / Created by the sun online YHY on 2017/3/1. / / Copyright © 2017 The sun online. All rights reserved. / / @ objc protocol YHYCollectionViewLayoutDelegate {/ / the number of columns of waterFall func columnOfWaterFall (_ CollectionView: UICollectionView) -> Int // The height of each item func waterFall(_ collectionView: waterFall) UICollectionView, layout waterFallLayout: YHYCollectionViewLayout, heightForItemAt indexPath: IndexPath) -> CGFloat } import UIKit class YHYCollectionViewLayout: UICollectionViewLayout { var delegate: YHYCollectionViewLayoutDelegate? @ibInspectable var columnCount: CGFloat = 2 @ibInspectable var columnSpacing: 0 CGFloat = 0 // lineSpacing default is 0 @ibinspectable var lineSpacing: // The spacing between section and collectionView is (0,0,0,0). UIEdgeInsets = UIEdgeInsets.zero // setctionTop @IBInspectable var sectionTop: CGFloat = 0 { willSet { sectionInsets.top = newValue } } @IBInspectable var sectionBottom: CGFloat = 0 { willSet { sectionInsets.bottom = newValue } } @IBInspectable var sectionLeft: CGFloat = 0 { willSet { sectionInsets.left = newValue } } @IBInspectable var sectionRight: CGFloat = 0 {willSet {sectionInsets. Right = newValue}} private var columnHeights: [Int: CGFloat] = [Int: CGFloat]() private var attributes: [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes] () / / custom initialization method (because it is to define the waterfall flow effect, So the initialization method sets the row spacing columnSpacing and section spacing.) init(lineSpacing: CGFloat,columnSpacing: CGFloat, sectionInsets: UIEdgeInsets) { super.init() self.lineSpacing = lineSpacing self.columnSpacing = columnSpacing self.sectionInsets = sectionInsets } required init? Subclasses must override this method and use it to return the width and height of the contents of the collectionView. These values represent the width and height of all content, not just the currently visible content. The collectionView uses this information to configure its own content size for scrolling purposes. override var collectionViewContentSize: CGSize { var maxHeight: CGFloat = 0for height in columnHeights.values {
   if height > maxHeight {
    maxHeight = height
   }
  }
  returnCGSize(width: collectionView? .frame.width ?? 0, height: MaxHeight + sectionInsets. Bottom)} // Override the prepare method, which must be used to tell layout to change the current layout. Override func can also be used to prepareprepare() { super.prepare() guard collectionView ! = nilelse {
   return
  }
  
  if letcolumnCount = delegate? .columnOfWaterFall(collectionView!) {for i in0.. <columnCount { columnHeights[i] = sectionInsets.top } }letitemCount = collectionView! .numberOfItems(inSection: 0)
  attributes.removeAll()
  for i in0.. <itemCount {if letatt = layoutAttributesForItem(at: IndexPath.init(row: i, section: 0)) {attributes.append(att)}}} // Override the layoutAttributesForItem method used to calculate the size of each cell subclass must override this method and use it to return layout information for items in the collection view. You can use this method to provide layout information only for items with corresponding cells. override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {if letCollectionView = collectionView {// Get the attributes of the item according to indexPathlet attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath) // Get the width of collectionViewlet width = collectionView.frame.width
   
   //
   if letcolumnCount = delegate? ColumnOfWaterFall (collectionView) {// columnCount is the number of columns in the collectionView item. Guard columnCount > 0else {
     returnNil} // item width = (colelctionView width - margin - column spacing)/column number // Total item width for each lineletTotalWidth = (width-sectionInsets. Left-sectioninsets. Right - (CGFloat(columnCount) -1) * columnSpacing) // Each item The width of thelet itemWidth = totalWidth / CGFloat(columnCount)
    print(itemWidth) // Calculates the height of the itemletitemheight = delegate? .waterFall(collectionView, layout: self, heightForItemAt: indexPath) ?? Var minIndex = 0for column in columnHeights {
     ifcolumn.value < columnHeights[minIndex] ?? 0 {minIndex = column.key}} // Calculate the x value of item based on the number of columns in the shortest columnletItemX = sectionInsets. Left + (columnSpacing + itemWidth) * CGFloat(minIndex) // Y value of item = maximum length of shortest column + row spacingletitemY = (columnHeights[minIndex] ?? 0) + lineSpacing // Set attributes to frame attribute.frame = CGRect(x: itemX, y: itemY, width: itemWidth, height: ColumnHeights [minIndex] = attribute.frame. MaxY} columnHeights[minIndex] = attribute.frame.return attribute
    }
  returnNil} // Subclasses must override this method and use it to return layout information for all items where the view intersects the specified rectangle. Your implementation should return the attributes of all visual elements, including cells, complementary views, and decorative views. // When creating layout Attributes, always create a property object representing the correct element type (cell, complement, or decoration). The collection view distinguishes properties of each type and uses that information to decide which views to create and how to manage them. override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  return attributes
 }
}

Copy the code

How to customize the code is explained in detail. The layout of the collectionView is reconstructed by overriding the methods of the parent class. The layout depends on how you calculate it when overriding the methods of the parent class. So the idea is very simple, the key lies in the layout of the calculation method, want to make a different style depends on the powerful calculation method to achieve.

Here’s another code for the circular layout: see how he calculates the layout:

/ / / / / / YHYCircleCollectionViewLayout swift mapps / / / / Created by the sun online YHY on 2017/3/1. / / Copyright © 2017 The sun online. All rights reserved. // import UIKit class YHYCircleCollectionViewLayout: UICollectionViewLayout { private var attributes: [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]() @IBInspectable var center: CGPoint! var itemCount: Int! var radius: CGFloat! override funcprepare() {super.prepare() = ectionView! .numberOfItems(inSection: 0) center = CGPoint(x: collectionView! .frame.width / 2, y: collectionView! .frame.height / 2) radius = min(collectionView! .frame.width, collectionView! .frame.height) / 4 attributes.removeAll()for i in0.. <itemCount {if let att = layoutAttributesForItem(at: IndexPath.init(row: i, section: 0)) {
    attributes.append(att)
   }
  }
 }
 
 override var collectionViewContentSize: CGSize {

  returnCGSize(width: collectionView? .frame.width ?? 0, height: (collectionView?.frame.width)!) } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {let attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath) attribute.size = CGSize(width: 60.0, height: 60.0) // The Angle of the current cell // Note the type conversionletAngle = 2 * CGFloat(M_PI) * CGFloat(indexpath.row)/CGFloat(itemCount) // attribute.center = CGPoint(x: center.x + radius*cos(angle), y: center.y + radius * sin(angle))return attribute
 }
 
 override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
  return attributes
 }
 
}

Copy the code