I'm trying to implement the UICollectionView
for custom cells in the shape of circle. Now by default the circles are aligned in the same way as the normal square cell: top circle and bottom circle are on the same vertical line. How can I change the alignment to this: the top circle and two circles below it form an equilateral triangle (positions of top circle and bottom circle are shifted by radius' length)? As below:
from OOO
OOO
OOO
to O O O
O O O (no spacing among the circles)
O O O
The basic idea is to create a custom UICollectionViewLayout
that implements:
collectionViewContentSize
, i.e., what's the size of the full, scrollable contentSize
of the collection view;
layoutAttributesForItem(at indexPath:)
, i.e., what are the key attributes (namely center
and size
) of a particular cell; and
layoutAttributesForElements(in rect:)
, i.e., what are the key attributes for cells that fall within this particular rect
... this will be used to identify which cells are visible at any given point in time as well as the attributes for those cells; this is basically an array of the attributes for the cells from the previous method, filtered down to just the ones within that rect
.
Thus, in Swift 3 you could do something like:
class AlternatingGridLayout: UICollectionViewLayout {
private var itemSize: CGSize!
private var numberOfItems: Int!
private var itemsPerRow: Int!
private var rows: Int!
private var circleViewCenterOffset: CGPoint!
private var radiusOfCircleViews: CGFloat!
private var insets: UIEdgeInsets!
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
radiusOfCircleViews = CGFloat(40.0)
itemSize = CGSize(width: radiusOfCircleViews * 2, height: radiusOfCircleViews * 2)
circleViewCenterOffset = CGPoint(x: 2 * radiusOfCircleViews * cos(.pi / 3),
y: 2 * radiusOfCircleViews * sin(.pi / 3))
numberOfItems = collectionView.numberOfItems(inSection: 0)
itemsPerRow = Int(floor((collectionView.bounds.width - radiusOfCircleViews) / CGFloat(2 * radiusOfCircleViews)) + 0.5)
rows = (numberOfItems - 1) / itemsPerRow + 1
let excess = collectionView.bounds.width - (CGFloat(itemsPerRow) * radiusOfCircleViews * 2 + circleViewCenterOffset.x)
insets = UIEdgeInsets(top: 10, left: excess / 2, bottom: 10, right: excess / 2)
}
override var collectionViewContentSize: CGSize {
return CGSize(width: collectionView!.bounds.width,
height: 2 * radiusOfCircleViews + CGFloat(rows - 1) * circleViewCenterOffset.y + insets.top + insets.bottom)
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.center = centerForItem(at: indexPath)
attributes.size = itemSize
return attributes
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return (0 ..< collectionView!.numberOfItems(inSection: 0)).map { IndexPath(item: $0, section: 0) }
.filter { rect.intersects(rectForItem(at: $0)) }
.compactMap { self.layoutAttributesForItem(at: $0) } // `flatMap` in Xcode versions before 9.3
}
private func centerForItem(at indexPath: IndexPath) -> CGPoint {
let row = indexPath.item / itemsPerRow
let col = indexPath.item - row * itemsPerRow
var x: CGFloat = radiusOfCircleViews + CGFloat(col) * (radiusOfCircleViews * 2)
let y: CGFloat = radiusOfCircleViews + CGFloat(row) * (circleViewCenterOffset.y)
if row % 2 == 0 {
x += circleViewCenterOffset.x
}
return CGPoint(x: x + insets.left, y: y + insets.top)
}
private func rectForItem(at indexPath: IndexPath) -> CGRect {
let center = centerForItem(at: indexPath)
return CGRect(x: center.x - radiusOfCircleViews, y: center.y - radiusOfCircleViews, width: radiusOfCircleViews * 2, height: radiusOfCircleViews * 2)
}
}
That yields:
Clearly, customize this as you see fit, but it illustrates the basic idea.
In my original answer, below, I assumed you wanted to see these cells in a circle, as shown in WWDC 2012 video Advanced Collection Views and Building Custom Layouts (about 40+ minutes into the video). See that below.
For example, in Swift 3:
class CircleLayout: UICollectionViewLayout {
private var center: CGPoint!
private var itemSize: CGSize!
private var radius: CGFloat!
private var numberOfItems: Int!
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
center = CGPoint(x: collectionView.bounds.midX, y: collectionView.bounds.midY)
let shortestAxisLength = min(collectionView.bounds.width, collectionView.bounds.height)
itemSize = CGSize(width: shortestAxisLength * 0.1, height: shortestAxisLength * 0.1)
radius = shortestAxisLength * 0.4
numberOfItems = collectionView.numberOfItems(inSection: 0)
}
override var collectionViewContentSize: CGSize {
return collectionView!.bounds.size
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
let angle = 2 * .pi * CGFloat(indexPath.item) / CGFloat(numberOfItems)
attributes.center = CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
attributes.size = itemSize
return attributes
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return (0 ..< collectionView!.numberOfItems(inSection: 0))
.compactMap { item -> UICollectionViewLayoutAttributes? in // `flatMap` in Xcode versions prior to 9.3
self.layoutAttributesForItem(at: IndexPath(item: item, section: 0))
}
}
}
Then you can simply set the collectionViewLayout
and then implement the standard UICollectionViewDataSource
methods.
class ViewController: UICollectionViewController {
var numberOfCells = 10
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.collectionViewLayout = CircleLayout()
// just for giggles and grins, let's show the insertion of a cell
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.collectionView?.performBatchUpdates({
self.numberOfCells += 1
self.collectionView?.insertItems(at: [IndexPath(item: 0, section: 0)])
}, completion: nil)
}
}
}
// MARK: UICollectionViewDataSource
extension ViewController {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return numberOfCells
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CircleCell", for: indexPath)
return cell
}
}
That yields:
See https://github.com/robertmryan/CircularCollectionView for sample.
Note, you mentioned that you want "no spacing among the circles", so just adjust the radius
and/or itemSize
accordingly to get the layout you want.