Created
April 9, 2022 10:40
-
-
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
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
// | |
// 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