Creating Reusable Views with Custom Flow Layout in Swift

Beyza Sığınmış
4 min readMay 3, 2023

--

In this article, we will discuss how to create reusable views in iOS applications using a custom flow layout called SectionBackgroundFlowLayout. This custom layout helps us to add backgrounds to the sections in a UICollectionView, making the interface more visually appealing and structured.

Let’s dive into the code to understand each part of the SectionBackgroundFlowLayout and its related classes.

SectionBackgroundFlowLayout Extension:

extension SectionBackgroundFlowLayout {
enum Constant {
static let decorationViewOfKind: String = "DecorationViewCollectionReusableView"
}
}

This extension contains an enumeration with a constant string to represent the decoration view kind, which is used to register and dequeue the decoration view in the layout.

SectionBackgroundFlowLayout Class:

final class SectionBackgroundFlowLayout: UICollectionViewFlowLayout {
...
}

SectionBackgroundFlowLayout is a custom layout class that subclasses UICollectionViewFlowLayout. This class overrides several methods to add decoration views (section backgrounds) to each section in the UICollectionView.

Initialization and Registration:

override init() {
super.init()
register(DecorationViewCollectionReusableView.self, forDecorationViewOfKind: Constant.decorationViewOfKind)
}

In the initializer, we register the DecorationViewCollectionReusableView class for the decoration view kind.

Preparing the Layout:

override func prepare() {
guard let collectionView = collectionView else { return }
let numberOfSections = collectionView.numberOfSections

decorationViewAttributes.removeAll()
for section in 0..<numberOfSections {
let numberOfItems = collectionView.numberOfItems(inSection: section)
guard numberOfItems > 0,
let firstItem = layoutAttributesForItem(at: IndexPath(item: 0, section: section)),
let lastItem = layoutAttributesForItem(at: IndexPath(item: numberOfItems - 1, section: section)) else {
continue
}

var sectionFrame = firstItem.frame.union(lastItem.frame)
sectionFrame.origin.x = .zero
sectionFrame.origin.y -= sectionInset.top
sectionFrame.size.width = collectionView.frame.width
sectionFrame.size.height += sectionInset.top + sectionInset.bottom

let attribute = DecorationViewCollectionViewLayoutAttributes(
forDecorationViewOfKind: Constant.decorationViewOfKind,
with: IndexPath(item: 0, section: section)
)

attribute.frame = sectionFrame
attribute.zIndex = -1
attribute.sectionBackgroundColor = .pink
decorationViewAttributes.append(attribute)
}

In the prepare method, we calculate the frame for each section's decoration view and create an instance of DecorationViewCollectionViewLayoutAttributes with the calculated frame and other attributes.

We get the number of items in the current section and use another guard statement to ensure that the section has at least one item. If not, we skip to the next iteration of the loop. We also retrieve the layout attributes for the first and last items in the section.

We calculate the frame for the decoration view (section background) by taking the union of the frames of the first and last items in the section. We then adjust the origin and size of the frame to account for the section insets.

We create a new instance of DecorationViewCollectionViewLayoutAttributes for the decoration view in the current section.

We set the frame, zIndex, and sectionBackgroundColor properties of the layout attributes. The zIndex is set to -1 to ensure the decoration view appears behind other elements, such as cells and supplementary views.

Finally, we append the created layout attributes to the decorationViewAttributes array. This array is used later in the layoutAttributesForElements(in:) method to return layout attributes for decoration views that intersect a given rectangle.

Returning Layout Attributes:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
var allAttributes = [UICollectionViewLayoutAttributes]()
allAttributes.append(contentsOf: attributes)
for attribute in decorationViewAttributes {
if rect.intersects(attribute.frame) {
allAttributes.append(attribute)
}
}
return allAttributes
}

The layoutAttributesForElements(in:) method returns an array of layout attributes for all the elements (including decoration views) that intersect the given rectangle.

  • First, we call the superclass’s implementation (super.layoutAttributesForElements(in: rect)) to retrieve the layout attributes for all cells and supplementary views that intersect the given rect.
  • Next, we create a new array called allAttributes to store all the layout attributes.
  • We append the layout attributes we got from the superclass’s implementation to the allAttributes array.
  • We then iterate through the decorationViewAttributes array, which contains the layout attributes for all decoration views (section backgrounds) in the layout.
  • For each decoration view attribute, we check if its frame intersects the given rect. If it does, we append the attribute to the allAttributes array.
  • Finally, we return the allAttributes array containing the layout attributes for cells, supplementary views, and decoration views that intersect the given rect.

Layout Attributes for Decoration View:

override func layoutAttributesForDecorationView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if elementKind == Constant.decorationViewOfKind {
let atts = UICollectionViewLayoutAttributes(forDecorationViewOfKind: Constant.decorationViewOfKind, with: indexPath)
return atts
}
return nil
}

This method returns layout attributes specifically for decoration views.

DecorationViewCollectionViewLayoutAttributes Class:

final class DecorationViewCollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
var sectionBackgroundColor: UIColor?
}

This custom class subclasses UICollectionViewLayoutAttributes to include an additional property, sectionBackgroundColor, which represents the background color of the section.

DecorationViewCollectionReusableView Class:

final class DecorationViewCollectionReusableView: UICollectionReusableView {
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
if let attribute = layoutAttributes as? DecorationViewCollectionViewLayoutAttributes {
backgroundColor = attribute.sectionBackgroundColor
}
}
}

This class is a reusable view that represents the section background. The apply(_:) method applies the layout attributes (including the background color) to the decoration view.

Usage:

To use the SectionBackgroundFlowLayout in your project, simply create an instance of it and set it as the layout for your UICollectionView:

let layout = SectionBackgroundFlowLayout()
collectionView.collectionViewLayout = layout

This will result in a UICollectionView with section backgrounds, making the interface more organized and visually appealing.

Conclusion:

In this article, we have explored how to create reusable views for section backgrounds in a UICollectionView using a custom flow layout called SectionBackgroundFlowLayout. This approach not only improves the visual appearance of the UICollectionView but also promotes code reusability and maintainability.

Thank you for taking the time to read. If you have any feedback or would like to get in touch, please feel free to reach out. ✨🤘🏻

References

--

--