Skip to content

Instantly share code, notes, and snippets.

@DanielCardonaRojas
Last active July 10, 2020 22:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DanielCardonaRojas/2b8b1ada1694289e76545ef2482ee949 to your computer and use it in GitHub Desktop.
Save DanielCardonaRojas/2b8b1ada1694289e76545ef2482ee949 to your computer and use it in GitHub Desktop.
Reference to create custom layout for UICollectionView
//
// CarouselLayout.swift
// Spotit
//
// Created by Daniel Cardona Rojas on 10/12/19.
// Copyright © 2019 Daniel Cardona. All rights reserved.
//
import UIKit
protocol CarouselLayoutDelegate: class {
func carouselLayout(_ layout: CarouselLayout, willSnapTo indexPath: IndexPath)
}
class CarouselLayout: UICollectionViewLayout {
// MARK: Public vars
public weak var delegate: CarouselLayoutDelegate?
public var itemSpacing: CGFloat = 15
public var itemWidthFactor: CGFloat = 0.75
public var shrinkConstant: CGFloat = 80
lazy var itemWidth: CGFloat = {
guard let collectionView = collectionView else { return 0 }
let insets = collectionView.contentInset
return collectionView.bounds.width * self.itemWidthFactor
}()
var viewPortCenterX: CGFloat? {
guard let offset = collectionView?.contentOffset,
let width = collectionView?.contentSize.width
else {
return nil
}
return offset.x + width / 2
}
var viewPortRange: (CGFloat, CGFloat)? {
guard let offset = collectionView?.contentOffset,
let width = collectionView?.contentSize.width
else {
return nil
}
return (offset.x, offset.x + width)
}
// MARK: State vars
private var cache = [UICollectionViewLayoutAttributes]()
private var contentHeight: CGFloat {
guard let collectionView = collectionView else { return 0 }
let insets = collectionView.contentInset
return collectionView.bounds.height - (insets.bottom + insets.top)
}
fileprivate var contentWidth: CGFloat = 0
// MARK: Helpers
private func findClosestAttributes(toXPosition xPosition: CGFloat)
-> UICollectionViewLayoutAttributes?
{
let sortedByDistance = cache.sorted(by: {
abs($0.center.x - xPosition) < abs($1.center.x - xPosition)
})
return sortedByDistance.first
}
private func layoutAttribute(
for indexPath: IndexPath, previousLayoutAttribute: UICollectionViewLayoutAttributes?
) -> UICollectionViewLayoutAttributes {
let remainingSpace = collectionView.map({ $0.bounds.width - itemWidth }) ?? 60
let leftSpace = max(0, remainingSpace / 2)
let layoutAttribute = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let width = itemWidth
let previousEndposition = previousLayoutAttribute.map({ $0.frame.origin.x + $0.frame.width }
)
let xPosition = (previousEndposition ?? 0) + leftSpace
let range = (xPosition, xPosition + width)
let vpRange = (collectionView!.contentOffset.x, collectionView!.contentOffset.x + width)
let overlap = min(range.1, vpRange.1) - max(range.0, vpRange.0)
let factor = overlap > 0 && overlap < width ? overlap / width : 0
let rect = CGRect(
x: xPosition,
y: 0,
width: itemWidth - (shrinkConstant * (1 - factor)),
height: contentHeight)
layoutAttribute.frame = rect
return layoutAttribute
}
// MARK: - Overrides
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
super.prepare()
guard let collectionView = self.collectionView else {
return
}
collectionView.decelerationRate = .fast
if collectionView.numberOfSections == 0 {
return
}
var previousLayoutAttributes: UICollectionViewLayoutAttributes?
for idx in 0..<collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: idx, section: 0)
let attribute = layoutAttribute(
for: indexPath, previousLayoutAttribute: previousLayoutAttributes)
contentWidth = max(contentWidth, attribute.frame.maxX)
cache.append(attribute)
previousLayoutAttributes = attribute
}
}
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint
) -> CGPoint {
guard let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
let midX: CGFloat = collectionView.bounds.size.width / 2
guard
let closestAttribute = findClosestAttributes(
toXPosition: proposedContentOffset.x + midX)
else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
delegate?.carouselLayout(self, willSnapTo: closestAttribute.indexPath)
return CGPoint(x: closestAttribute.center.x - midX, y: proposedContentOffset.y)
}
override func layoutAttributesForElements(in rect: CGRect)
-> [UICollectionViewLayoutAttributes]?
{
return cache.filter { attr in
attr.frame.intersects(rect)
}
}
override func layoutAttributesForItem(at indexPath: IndexPath)
-> UICollectionViewLayoutAttributes?
{
cache.first(where: { $0.indexPath == indexPath })
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
if newBounds.size != collectionView?.bounds.size { cache.removeAll() }
return true
}
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext) {
if context.invalidateDataSourceCounts { cache.removeAll() }
super.invalidateLayout(with: context)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment