Skip to content

Instantly share code, notes, and snippets.

@shawn-frank
Created April 9, 2022 10:40
Show Gist options
  • Save shawn-frank/451b0790749475c67b9953f5857c2b8e to your computer and use it in GitHub Desktop.
Save shawn-frank/451b0790749475c67b9953f5857c2b8e to your computer and use it in GitHub Desktop.
This is a small example created that demonstrates the use of a custom UICollectionViewFlowLayout to create cells that overlap each other. This demo was created in response to this StackOverflow question: https://stackoverflow.com/q/71784968/1619193
//
// OverlapViewController.swift
// TestApp
//
// Created by Shawn Frank on 09/04/2022.
//
import UIKit
class OverlapViewController: UIViewController {
// The collection view
private var collectionView: UICollectionView!
override func viewDidLoad() {
title = "Overlap Cell"
view.backgroundColor = .white
configureCollectionView()
}
private func configureCollectionView()
{
collectionView = UICollectionView(frame: CGRect.zero,
collectionViewLayout: createLayout())
collectionView.backgroundColor = .white
collectionView.register(UICollectionViewCell.self,
forCellWithReuseIdentifier: "cell")
collectionView.dataSource = 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.bottomAnchor.constraint(equalTo: view.bottomAnchor)
.isActive = true
}
private func createLayout() -> UICollectionViewFlowLayout
{
let customLayout = OverlapFlowLayout()
customLayout.itemSize = CGSize(width: 100, height: 100)
return customLayout
}
}
extension OverlapViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
return 20
}
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell",
for: indexPath)
cell.backgroundColor = .systemBlue
if indexPath.item == 0 {
cell.backgroundColor = .red
}
return cell
}
}
fileprivate class OverlapFlowLayout: UICollectionViewFlowLayout {
// Y Offset for the cells after the first
let initialYOffset: CGFloat = 30
// X Offset the cells after the first
let xOffset: CGFloat = 20
// Vertical spacing between cells
let cellSpacing: CGFloat = 10
// Store the updated content height
var contentHeight: CGFloat = .zero
// Cache layout attributes for the cells
private var cellLayoutCache: [IndexPath: UICollectionViewLayoutAttributes] = [:]
override func prepare() {
guard let collectionView = collectionView else { return }
// Only calculate if the cache is empty
guard cellLayoutCache.isEmpty else { return }
let sections = 0 ... collectionView.numberOfSections - 1
// Set the content height as the initial Y offset
// so that the second cell overlaps the first
contentHeight = initialYOffset
// Loop through all the sections
for section in sections {
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 attribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
var itemFrame: CGRect
// Check if it is the first item
if attribute.indexPath.item == 0 {
itemFrame = CGRect(x: 0,
y: 0,
width: itemSize.width,
height: itemSize.height)
attribute.zIndex = -1
}
else {
// Use the current content height as the y position
// so the items form one column
itemFrame = CGRect(x: 20,
y: contentHeight,
width: itemSize.width,
height: itemSize.height)
// Update the y offset by the cell height and spacing
contentHeight += itemSize.height + cellSpacing
attribute.zIndex = 0
}
// Set the frame for the current item
attribute.frame = itemFrame
cellLayoutCache[indexPath] = attribute
}
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// Retrieve the attributes from the cache
return cellLayoutCache[indexPath]
}
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) }
return itemAttributes
}
override var collectionViewContentSize: CGSize {
guard let collectionView = collectionView else { return .zero }
// Update the content size with the new height
return CGSize(width: collectionView.bounds.width,
height: contentHeight)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment