Created
March 21, 2022 17:03
-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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