Skip to content

Instantly share code, notes, and snippets.

@shawn-frank
Created March 21, 2022 17:03
Show Gist options
  • Save shawn-frank/e5f6324f42f2ac1366f502df84945ed7 to your computer and use it in GitHub Desktop.
Save shawn-frank/e5f6324f42f2ac1366f502df84945ed7 to your computer and use it in GitHub Desktop.
This is an example of creating a custom UICollectionViewFlowLayout with a decoration view in order to give each section a border for iOS using Swift. This was in response to this stack overflow question: https://stackoverflow.com/questions/71552265/is-there-a-way-to-give-outer-border-in-every-section-of-uicollctionview?noredirect=1#comment126463…
//
// CustomLayoutVC.swift
// TestApp
//
// Created by Shawn Frank on 21/03/2022.
//
import UIKit
// This is used as the header and footer of each section
fileprivate class HeaderFooterView: UICollectionReusableView
{
let title = UILabel()
static let identifier = "HeaderFooterViewID"
override init(frame: CGRect) {
super.init(frame: frame)
layoutInterface()
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)!
layoutInterface()
}
func layoutInterface() {
backgroundColor = .clear
title.translatesAutoresizingMaskIntoConstraints = false
title.backgroundColor = .clear
title.textAlignment = .center
title.textColor = .black
addSubview(title)
addConstraints([
title.leadingAnchor.constraint(equalTo: leadingAnchor),
title.topAnchor.constraint(equalTo: topAnchor),
title.trailingAnchor.constraint(equalTo: trailingAnchor),
title.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
// This is the decoration view behind every section
class SectionBackgroundView : UICollectionReusableView {
static let DecorationViewKind = "SectionBackgroundIdentifier"
override init(frame: CGRect) {
super.init(frame: frame)
// Customize the settings to what you want
backgroundColor = .clear
layer.borderWidth = 5.0
layer.borderColor = UIColor.blue.cgColor
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// This is the custom flow layout
class BorderedFlowLayout: UICollectionViewFlowLayout {
override init() {
super.init()
// Register your decoration view for the layout
register(SectionBackgroundView.self,
forDecorationViewOfKind: SectionBackgroundView.DecorationViewKind)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutAttributesForDecorationView(ofKind elementKind: String,
at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if elementKind == SectionBackgroundView.DecorationViewKind {
guard let collectionView = collectionView else { return nil }
// Initialize a UICollectionViewLayoutAttributes for a DecorationView
let decorationAttributes
= UICollectionViewLayoutAttributes(forDecorationViewOfKind: SectionBackgroundView.DecorationViewKind,
with:indexPath)
// Set it behind other views
decorationAttributes.zIndex = 2
let numberOfItemsInSection
= collectionView.numberOfItems(inSection: indexPath.section)
// Get the first and last item in the section
let firstItem = layoutAttributesForItem(at: IndexPath(item: 0, section: indexPath.section))
let lastItem = layoutAttributesForItem(at: IndexPath(item: (numberOfItemsInSection - 1),
section: indexPath.section))
// The difference between the maxY of the last item and
// the the minY of the first item is the height of the section
let height = lastItem!.frame.maxY - firstItem!.frame.minY
// Set the frame of the decoration view for the section
decorationAttributes.frame = CGRect(x: 0,
y: firstItem!.frame.minY,
width: collectionView.bounds.width,
height: height)
return decorationAttributes
}
return nil
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Get all the UICollectionViewLayoutAttributes for the current view port
var attributes = super.layoutAttributesForElements(in: rect)
// Filter to get all the different sections
let sectionAttributes
= attributes?.filter { $0.indexPath.item == 0 } ?? []
// Loop through the different sections
for sectionAttribute in sectionAttributes {
// Create decoration attributes for the current section
if let decorationAttributes
= self.layoutAttributesForDecorationView(ofKind: SectionBackgroundView.DecorationViewKind,
at: sectionAttribute.indexPath) {
// Add the decoration attributes for a section if it is in the current viewport
if rect.intersects(decorationAttributes.frame) {
attributes?.append(decorationAttributes)
}
}
}
return attributes
}
}
class CustomLayoutVC: UIViewController {
private var collectionView: UICollectionView!
private var cellType: AnyObject?
let numberOfColumns = 3
let horizontalPadding: CGFloat = 10
override func viewDidLoad() {
super.viewDidLoad()
title = "Collection View"
view.backgroundColor = .white
configureCollectionView()
}
private func configureCollectionView() {
collectionView = UICollectionView(frame: CGRect.zero,
collectionViewLayout: createLayout())
collectionView.backgroundColor = .white
collectionView.register(UICollectionViewCell.self,
forCellWithReuseIdentifier: "cell")
// You can ignore the header and footer views as you probably already did this
collectionView.register(HeaderFooterView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: HeaderFooterView.identifier)
collectionView.register(HeaderFooterView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter,
withReuseIdentifier: HeaderFooterView.identifier)
collectionView.dataSource = self
collectionView.delegate = self
view.addSubview(collectionView)
// Auto layout config to pin collection view to the edges of the view
// You can ignore this for your solution
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.leadingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 0).isActive = true
collectionView.topAnchor
.constraint(equalTo: view.topAnchor,
constant: 0).isActive = true
collectionView.trailingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor,
constant: 0).isActive = true
collectionView.bottomAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor,
constant: 0).isActive = true
}
private func createLayout() -> UICollectionViewFlowLayout {
let flowLayout = BorderedFlowLayout()
flowLayout.minimumLineSpacing = 10
flowLayout.minimumInteritemSpacing = 10
flowLayout.scrollDirection = .vertical
flowLayout.sectionInset = UIEdgeInsets(top: 10,
left: horizontalPadding,
bottom: 10,
right: horizontalPadding)
return flowLayout
}
}
extension CustomLayoutVC: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 4
}
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return 3 * (section + 1)
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView
.dequeueReusableCell(withReuseIdentifier:"cell",
for: indexPath)
cell.backgroundColor = .red
return cell
}
}
extension CustomLayoutVC: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView {
switch kind {
case UICollectionView.elementKindSectionHeader:
let headerView
= collectionView.dequeueReusableSupplementaryView(ofKind: kind,
withReuseIdentifier: HeaderFooterView.identifier,
for: indexPath) as! HeaderFooterView
headerView.backgroundColor = UIColor.yellow
headerView.title.text = "Header Section \(indexPath.section + 1)"
return headerView
case UICollectionView.elementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind,
withReuseIdentifier: HeaderFooterView.identifier,
for: indexPath) as! HeaderFooterView
footerView.backgroundColor = UIColor.orange
footerView.title.text = "Footer Section \(indexPath.section + 1)"
return footerView
default:
assert(false, "Unexpected element kind")
}
}
}
extension CustomLayoutVC: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
if let collectionViewLayout = collectionViewLayout as? UICollectionViewFlowLayout {
let totalHorizontalPadding = horizontalPadding * CGFloat(numberOfColumns)
let interItemSpacing = collectionViewLayout.minimumInteritemSpacing * CGFloat(numberOfColumns - 1)
let availableWidth = collectionView.bounds.size.width - totalHorizontalPadding - interItemSpacing
let cellWidth = availableWidth / CGFloat(numberOfColumns)
return CGSize(width: cellWidth, height: cellWidth)
}
return CGSize.zero
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 100)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForFooterInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 100)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment