Add Custom Header to Collection View Swift

add custom header to collection view swift

You need to call viewForSupplementaryElementOfKind like this:

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {

switch kind {
case UICollectionElementKindSectionHeader:
let reusableview = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "HCollectionReusableView", for: indexPath) as! HCollectionReusableView

reusableview.frame = CGRect(0 , 0, self.view.frame.width, headerHight)
//do other header related calls or settups
return reusableview


default: fatalError("Unexpected element kind")
}
}

This way you can initialise and show the header

Another way of setting the UICollectionViewHeader frame is by extending UICollectionViewDelegateFlowLayout like this:

extension UIViewController: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 100) //add your height here
}
}

This removes the need to call :

reusableview.frame = CGRect(0 , 0, self.view.frame.width, headerHight)

in the above mentioned

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView

Remember to register the HeaderView after you initialise your UICollectionView by calling:

collectionView.register(UINib(nibName: HCollectionReusableView.nibName, bundle: nil), forSupplementaryViewOfKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "HCollectionReusableView")

Swift 4.1 Update

UICollectionElementKindSectionHeader has been renamed to UICollectionView.elementKindSectionHeader

How to make both header and footer in collection view with swift

You can make an UICollectionViewController to handle the UICollectionView and in Interface Builder activate the Footer and Header sections, then you can use the following method for preview in you UICollectionView the two sections added :

override func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {

switch kind {

case UICollectionView.elementKindSectionHeader:

let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath)

headerView.backgroundColor = UIColor.blue
return headerView

case UICollectionView.elementKindSectionFooter:
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath)

footerView.backgroundColor = UIColor.green
return footerView

default:

assert(false, "Unexpected element kind")
}
}

In the above code I put the identifier for the footer and header as Header and Footer for example, you can do it as you want. If you want to create a custom header or footer then you need to create a subclass of UICollectionReusableView for each and customize it as you want.

You can register your custom footer and header classes in Interface Builder or in code with:

registerClass(myFooterViewClass, forSupplementaryViewOfKind: UICollectionElementKindSectionFooter, withReuseIdentifier: "myFooterView")

How to add UICollectionView Header

You're using the wrong method to register your header.
You're also dequeuing using the wrong method. Since you're registering your header as a forSupplementaryViewOfKind -- You have to use deque the header using 'dequeueReusableSupplementaryView' method instead of 'dequeueReusableCell'

 override func viewDidLoad() {
collectionView?.register(Header.self, forSupplementaryViewOfKind:
UICollectionElementKindSectionHeader, withReuseIdentifier: headerId)
}


override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind:
String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier:
headerId, for: indexPath) as! Header
return header
}

How to use custom UICollectionReusableView as section header of collection view?

Once you assign proper class and identifier in your Xib file, then it will work without crashes.

Programmatically create UICollectionView with custom headers

So I figured it out, with inspiration from Mohamad Farhand.

The problem was that I had to register the subclass itself with the collectionView, instead of UICollectionReusableView.self, I used the instance of the subclass someView.. So this solved my problem:

collectionView.registerClass(SupView.self, forSupplementaryViewOfKind: UICollectionElementKindSectionHeader , withReuseIdentifier: "someRandonIdentifierString")

And how to initialize the view:

someView = collectionView.dequeueReusableSupplementaryViewOfKind(kind, withReuseIdentifier: "someRandonIdentifierString", forIndexPath: indexPath) as! SupView

Need Header on Top for Horizontal UICollectionView UIKIT

In short, creating a custom UICollectionViewLayout is one way to get the results you want. This is quite a complex topic so the explanation is quite long.

Before adding some code, you will need to understand the different components in a UICollectionViewLayout such as cells, supplementary views and decoration views. Check this out to know more

In your example, above the headers are called Supplementary viewswhich are of type UICollectionReusableView and the cells are, well, your cells of type UICollectionViewCells.

When assigning a layout to a collection view, the collection view makes a sequence of calls to its layout to understand how to lay the views out.

Here is an example of this I found of this:

UICollectionView Custom Layout

source: https://www.raywenderlich.com/4829472-uicollectionview-custom-layout-tutorial-pinterest

In your custom layout, you must override these to create the layout you desire:

  • prepare()
  • collectionViewContentSize
  • layoutAttributesForElements(in:)
  • layoutAttributesForItem(at:)
  • layoutAttributesForSupplementaryView(ofKind:, at:)
  • invalidateLayout()
  • shouldInvalidateLayout(forBoundsChange)

Setting up the layout is a bit complex, mainly because of the math involved but at the end of the day, it is just about specifying frames (x, y, width, height) for the different cells and supplementary views at for different viewports.

Custom Layout example

First I created a very basic reusable view to be used as the header above the cells in each section and this just has a label in it

class HeaderView: UICollectionReusableView
{
let title = UILabel()

static let identifier = "CVHeader"

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 = .left
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)

])
}
}

Next I set up my collection view in a normal way, the only difference is I provided a custom layout class

// The collection view
private var collectionView: UICollectionView!

// A random data source
let colors: [UIColor] = [.systemBlue, .orange, .purple]

private func configureCollectionView()
{
collectionView = UICollectionView(frame: CGRect.zero,
collectionViewLayout: createLayout())

collectionView.backgroundColor = .lightGray

collectionView.register(UICollectionViewCell.self,
forCellWithReuseIdentifier: "cell")

// This is for the section titles
collectionView.register(HeaderView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: HeaderView.identifier)

collectionView.dataSource = self
collectionView.delegate = self

view.addSubview(collectionView)

// Auto layout config to pin collection view to the edges of the view
collectionView.translatesAutoresizingMaskIntoConstraints = false

collectionView.leadingAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor,
constant: 0).isActive = true

collectionView.topAnchor
.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,
constant: 0).isActive = true
collectionView.trailingAnchor

.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor,
constant: 0).isActive = true

collectionView.heightAnchor
.constraint(equalToConstant: 300).isActive = true
}

private func createLayout() -> HorizontalLayout
{
// Not flow layout, but our custom layout
let customLayout = HorizontalLayout()
customLayout.itemSpacing = 10
customLayout.sectionSpacing = 20
customLayout.itemSize = CGSize(width: 50, height: 50)

return customLayout
}

Data source and delegate also has nothing too different but I am adding it for completeness

extension ViewController: UICollectionViewDataSource
{
func numberOfSections(in collectionView: UICollectionView) -> Int
{
// random number
return 3
}

func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int
{
// random number
return 8 + section
}

func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView
.dequeueReusableCell(withReuseIdentifier:"cell",
for: indexPath)

cell.backgroundColor = colors[indexPath.section]

return cell
}

func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
referenceSizeForHeaderInSection section: Int) -> CGSize
{
return CGSize(width: 200, height: 50)
}
}

extension ViewController: UICollectionViewDelegate
{
func collectionView(_ collectionView: UICollectionView,
viewForSupplementaryElementOfKind kind: String,
at indexPath: IndexPath) -> UICollectionReusableView
{
let header
= collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: HeaderView.identifier,
for: indexPath) as! HeaderView

header.title.text = "Section \(indexPath.section)"

return header
}
}

Finally, the most complex part, creating a custom layout class. Start by looking and understanding the instance variables we keep track of, this will change based on the layout you want to create

class HorizontalLayout: UICollectionViewLayout
{
// Cache layout attributes for the cells
private var cellLayoutCache: [IndexPath: UICollectionViewLayoutAttributes] = [:]

// Cache layout attributes for the header
private var headerLayoutCache: [Int: UICollectionViewLayoutAttributes] = [:]

// Set a y offset so the items render a bit lower which
// leaves room for the title at the top
private var sectionTitleHeight = CGFloat(60)

// The content height of the layout is static since we're configuring horizontal
// layout. However, the content width needs to be calculated and returned later
private var contentWidth = CGFloat.zero

private var contentHeight: CGFloat
{
guard let collectionView = collectionView else { return 0 }

let insets = collectionView.contentInset

return collectionView.bounds.height - (insets.left + insets.right)
}

// Based on the height of the collection view, the interItem spacing and
// the item height, we can set
private var maxItemsInRow = 0

// Set the spacing between items & sections
var itemSpacing: CGFloat = .zero
var sectionSpacing: CGFloat = .zero

var itemSize: CGSize = .zero

override init()
{
super.init()
}

required init?(coder: NSCoder)
{
super.init(coder: coder)
}

We then need a function to help us figure out how many items can fit in 1 column based on the cell height, spacing and available space based on the collection view height. This will help us move the cell to the next column if there is no more space in the current column:

private func updateMaxItemsInColumn()
{
guard let collectionView = collectionView else { return }

let contentHeight = collectionView.bounds.height

let totalInsets
= collectionView.contentInset.top + collectionView.contentInset.bottom

// The height we have left to render the cells in
let availableHeight = contentHeight - sectionTitleHeight - totalInsets

// Set the temp number of items in a column as we have not
// accounted for item spacing
var tempItemsInColumn = Int(availableHeight / itemSize.height)

// Figure out max items allowed in a row based on spacing settings
while tempItemsInColumn != 0
{
// There is 1 gap between 2 items, 2 gaps between 3 items etc
let totalSpacing = CGFloat(tempItemsInColumn - 1) * itemSpacing

let finalHeight
= (CGFloat(tempItemsInColumn) * itemSize.height) + totalSpacing

if availableHeight < finalHeight
{
tempItemsInColumn -= 1
continue
}

break
}

maxItemsInRow = tempItemsInColumn
}

Next, we need a function to help us find the width of a section as we need to know how long the header view should be

private func widthOfSection(_ section: Int) -> CGFloat
{
guard let collectionView = collectionView else { return .zero }

let itemsInSection = collectionView.numberOfItems(inSection: section)

let columnsInSection = itemsInSection / maxItemsInRow

// There is 1 gap between 2 items, 2 gaps between 3 items etc
let totalSpacing = CGFloat(itemsInSection - 1) * itemSpacing

let totalWidth = (CGFloat(columnsInSection) * itemSize.width) + totalSpacing

return totalWidth
}

And as mentioned above, we need to override a few functions and properties. Let's start with prepare()

// This function gets called before the collection view starts the layout process
// load layout into the cache so it doesn't have to be recalculated each time
override func prepare()
{
guard let collectionView = collectionView else { return }

// Only calculate if the cache is empty
guard cellLayoutCache.isEmpty else { return }

updateMaxItemsInColumn()

let sections = 0 ... collectionView.numberOfSections - 1

// Track the x position of the items being drawn
var itemX = CGFloat.zero

// Loop through all the sections
for section in sections
{
var itemY = sectionTitleHeight
var row = 0

let headerFrame = CGRect(x: itemX,
y: 0,
width: widthOfSection(section),
height: sectionTitleHeight)

let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
with: IndexPath(item: 0, section: section))
attributes.frame = headerFrame
headerLayoutCache[section] = attributes

let itemsInSection = collectionView.numberOfItems(inSection: section)

// Generate valid index paths for all items in the section
let indexPaths = [Int](0 ... itemsInSection - 1).map
{
IndexPath(item: $0, section: section)
}

// Loop through all index paths and cache all the layout attributes
// so it can be reused later
for indexPath in indexPaths
{
let itemFrame = CGRect(x: itemX,
y: itemY,
width: itemSize.width,
height: itemSize.height)

let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = itemFrame
cellLayoutCache[indexPath] = attributes

contentWidth = max(contentWidth, itemFrame.maxX)

// last item in the section, update the x position
// to start the next section in a new column and also
// update the content width to add the section spacing
if indexPath.item == indexPaths.count - 1
{
itemX += itemSize.width + sectionSpacing
contentWidth = max(contentWidth, itemFrame.maxX + sectionSpacing)
continue
}

if row < maxItemsInRow - 1
{
row += 1
itemY += itemSize.height + itemSpacing
}
else
{
row = 0
itemY = sectionTitleHeight
itemX += itemSize.width + itemSpacing
}
}
}
}

The content size property

// We need to set the content size. Since it is a horizontal
// collection view, the height will be fixed. The width should be
// the max X value of the last item in the collection view
override var collectionViewContentSize: CGSize
{
return CGSize(width: contentWidth, height: contentHeight)
}

The three layout attribute functions

// This defines what gets shown in the rect (viewport) the user
// is currently viewing
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
{
// Get the attributes that fall in the current view port
let itemAttributes = cellLayoutCache.values.filter { rect.intersects($0.frame) }
let headerAttributes = headerLayoutCache.values.filter { rect.intersects($0.frame) }

return itemAttributes + headerAttributes
}

override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
{
return cellLayoutCache[indexPath]
}

override func layoutAttributesForSupplementaryView(ofKind elementKind: String,
at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
{
return headerLayoutCache[indexPath.section]
}

And finally the invalidate layout

// In invalidateLayout(), the layout of the elements will be changing
// inside the collection view. Here the attribute cache can be reset
override func invalidateLayout()
{
// Reset the attribute cache
cellLayoutCache = [:]
headerLayoutCache = [:]

super.invalidateLayout()
}

// Invalidating the layout means the layout needs to be recalculated from scratch
// which might need to happen when the orientation changes so we only want to
// do this when it is necessary since it is expensive
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool
{
guard let collectionView = collectionView else { return false }

return newBounds.height != collectionView.bounds.height
}

With all of this in place, this should give you this result:

Custom UICollectionView layout swift iOS UICollectionViewLayout flow layout customization

If for some reason you weren't able to add the code to the right sections, have a look at this same example with the complete source code here

While I cannot cover everything, here are 3 great tutorials you can look at after going through this answer to understand more in depth:

  1. Ray Weldenrich: UICollectionView Custom Layout Tutorial
  2. Building a Custom UICollectionViewLayout from Scratch
  3. Custom Collection View Layouts


Related Topics



Leave a reply



Submit