The original address: engineering.shopspring.com/custom-coll…

An overview of the

UICollectionView is one of the most powerful tools in iOS development. You can use it to create complex UIs that support scrolling, respond to click events, update layouts, and have excellent performance. In fact, it’s not surprising that almost every page in your application could/should support UICollectionView.

Unlike UITableView, UICollectionView gives you plenty of freedom to customize your own layout. Yeah, you can put things wherever you want. To take advantage of this feature, you will need to create your own custom layout.

Custom layouts are well documented in Apple and many other articles, but it’s easy to get lost in the details. In this article, we will provide a simple template to implement custom Layout. Along the way, we’ll discuss what layout is, what methods you should implement, and what you should do in those methods.

Note: * * * * before starting the exploration, is worth to take a look at apple’s default layout (UICollectionViewFlowLayout) are sufficient to meet your demand. This class is highly customizable and is likely to be suitable for most situations.

What would you say… What are you doing here?

What exactly does layout do, and how does it work with UICollectionView? You can think of UICollectionView as a rendering engine. It is responsible for creating views, displaying them on the screen, and handling events. However, to maximize its reusability, UICollectionView uses a large number of delegates to implement custom functionality. For example, it delegates the responsibility of creating cells to its data source. Similarly, it delegates responsibility for determining information such as the location of these cells to its Layout.

Finally, the layout is responsible for returning two pieces of information:

  1. How big is the content area? (For example, what is the size of the scrollable area?)
  2. Where are the cells and complement/decorate views and what are their sizes?

The first information is returned via the read-only property collectionView ContentSize (more on that later). The second is through UICollectionViewLayoutAttributes object returned. UICollectionViewLayoutAttributes according to its type (cell, supplement or decorate views) and indexPath oerlooked. Layout attributes encapsulate layout information about the corresponding cells and views (such as frames) and tell the Collection View where to put those visible elements. Your custom Layout object returns these properties through the methods described below, and your Collection View uses these properties to place visible elements in the specified location.

Create a Custom Layout

To create a Custom Layout, you must create a subclass of UICollectionViewLayout. Your subclass should override at least one of the following methods:

The core

  • prepareNot required, but highly recommended.
  • collectionViewContentSize
  • layoutAttributesForElements(in:)

Property requirements

  • layoutAttributesForItem(at:)
  • layoutAttributesForSupplementaryView(ofKind:at:)(If your layout supports additional elements)
  • layoutAttributesForDecorationView(ofKind:at:)(If your layout supports decorated views)

There are many ways to implement these methods. But in the end, you just need to return the appropriate content size and layout properties. Below, we’ll provide one way to do this — a simple template that can be customized by just modifying the prepare() method.

Rewrite the core layout method

When laying out your Collection View, Apple will call the following methods in the following order:

  • prepare()
  • collectionViewContentSize
  • layoutAttributesForElements(in:)

This constitutes the core layout process. (Core Layout Process)

prepare()

This method is called first during the core layout process, which is an opportunity for preprocessing.

In our implementation, this is the most important method, because this is where the magic (aka math) happens. In this method we will:

  • Determine the size and position of each cell, supplementart view and decoration view.
  • Calculate the content size of the slidable area in the Collection View.
  • Instantiate these layout properties and store them.

Basically, this method is where we decide what the layout is going to be.

Your subclasses have access to a property called collectionView that points to the Collection View to which the layout belongs. Use it to get information about sections, cells, and views. This information is combined with other model data to determine which data to put where (other data can be set directly to the layout or provided through delegates).

Remember, you cannot use the frame property of a cell or view. Because it hasn’t been calculated yet, that’s what we’re going to do.

After the calculation is complete, the results are stored for access by other methods. I suggest:

  • For Content Size, define a private CGSize property and set it to the calculated size.
  • For layout Attributes, define one to three private dictionaries to store the computed results — one for cells, one for supplementary Views (if you need them), and one for decoration views (if you need them). Each cell/view for you to create a UICollectionViewLayoutAttributes object, according to your math set its framework, and add it to the index path as the key to the dictionary.
private var computedContentSize: CGSize = .zero
private var cellAttributes = [IndexPath: UICollectionViewLayoutAttributes] ()override func prepare(a) {
  // Clear out previous results
  computedContentSize = .zero
  cellAttributes = [IndexPath: UICollectionViewLayoutAttributes] ()for section in 0 ..< collectionView.numberOfSections {
    for item in 0 ..< collectionView.numberOfItems(inSection: section) {
      let itemFrame = / /... Determine the frame of your cell...

      // Create the layout attributes and set the frame
      let indexPath = IndexPath(item: item, section: section)
      let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
      attributes.frame = itemFrame

      // Store the results
      cellAttributes[indexPath] = attributes
    }
  }
  
  computedContentSize = // Store computed content size
}
Copy the code

In short, for our implementation, your prepare() method should:

  1. Use self.collectionView to determine what you want to lay out.
  2. Figure out where all the elements go and how much space they need.
  3. Instantiate the appropriate UICollectionViewLayoutAttributes object.
  4. Store layout properties and calculate conetnt size by other means.

collectionContentSize

This value determines the size of the scrollable area in the Collection View. It is actually a read-only property of the Collection View Layout. Overrides its getter method to return the desired size.

For our implementation, return the content size calculated and stored in prepare().

private var computedContentSize: CGSize = .zero

override var collectionViewContentSize: CGSize {
  return computedContentSize
}
Copy the code

layoutAttributesForElements(in:)

This method will give you pass in a CGRect, and hope you return a UICollectionViewLayoutAttributes array of objects, the object corresponding to the part (and all) in the rectangular/view in the cell. (i.e. all the cells/views that intersect this rectangle)

In our implementation, we iterate over the layout properties created in Prepare (), checking their frames to see if they intersect the supplied rectangles. This can be easily determined using CGRect’s intersects(_:) method.

For any layout properties that the frame intersects with the given rectangle, place them in an array and return that array.

private var cellAttributes = [IndexPath: UICollectionViewLayoutAttributes] ()override func layoutAttributesForElements(in rect: CGRect)- > [UICollectionViewLayoutAttributes]? {
  var attributeList = [UICollectionViewLayoutAttributes] ()for (_, attributes) in cellAttributes {
    if attributes.frame.intersects(rect) {
      attributeList.append(attributes)
    }
  }

  return attributeList
}
Copy the code

The layout properties

As if this were not enough, your collection view may also require layout properties on specific index paths in addition to the core layout process. The following methods can satisfy this requirement:

  • layoutAttributesForItem(at:)

  • LayoutAttributesForSupplementaryView (ofKind: at:) (if your layout support supplementary views)

  • LayoutAttributesForDecorationView (ofKind: at:) (if your layout support decoration views)

Each of these methods takes an indexed path and returns the layout properties of the cell/view on that path.

For our implementation, simply use the index path provided to access the appropriate dictionary and return the result.

private var cellAttributes = [IndexPath: UICollectionViewLayoutAttributes] ()override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
  return cellAttributes[indexPath]
}
Copy the code

If your collection view doesn’t use a complement/decorator view, then these two methods return nil.

conclusion

To create a Custom Layout, all you need to do is:

  • Returns the size of the scrollable area in collectionViewContentSize
  • In layoutAttributesForElements (in), layoutAttributesForItem (ats), LayoutAttributesForSupplementaryView (ofKind: at:) and layoutAttributesForDecorationView (ofKind: at:) returned in the corresponding frame of all UICollectionViewLayoutAttributes object.

Here is our implementation template. To customize it, all you need to do is change prepare() to calculate the size and position required for your layout.

@objc class CustomCollectionViewLayout: UICollectionViewLayout {
  private var computedContentSize: CGSize = .zero
  private var cellAttributes = [IndexPath: UICollectionViewLayoutAttributes] ()override func prepare(a) {
    // Clear out previous results
    computedContentSize = .zero
    cellAttributes = [IndexPath: UICollectionViewLayoutAttributes] ()for section in 0 ..< collectionView.numberOfSections {
      for item in 0 ..< collectionView.numberOfItems(inSection: section) {
        let itemFrame = / /... Determine the frame of your cell...
        // Create the layout attributes and set the frame
        let indexPath = IndexPath(item: item, section: section)
        let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
        attributes.frame = itemFrame

        // Store the results
        cellAttributes[indexPath] = attributes
      }
    }

    computedContentSize = // Store computed content size
  }
  
  override var collectionViewContentSize: CGSize {
    return computedContentSize
  }
  
  override func layoutAttributesForElements(in rect: CGRect)- > [UICollectionViewLayoutAttributes]? {
    var attributeList = [UICollectionViewLayoutAttributes] ()for (_, attributes) in cellAttributes {
      if attributes.frame.intersects(rect) {
        attributeList.append(attributes)
      }
    }
    
    return attributeList
  }
  
  override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
    return cellAttributes[indexPath]
  }
  
}
Copy the code

Now you just need to instantiate your custom layout and set it to the layout properties of your collection view to see the results run.