iOS/Swift

[Swift] UICollectionView Custom Layout: A Spinning Wheel with Swift 5

HUISOO 2022. 11. 4. 13:58

UICollectionView Custom Layout Tutorial: A Spinning Wheel을 Swift 5로 작성한 글입니다.


1. UICollectionViewCell

class CollectionViewCell: UICollectionViewCell {
    override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
        super.apply(layoutAttributes)
        
        guard let layoutAttributes = layoutAttributes as? CollectionViewLayoutAttributes else { return }
        layer.anchorPoint = layoutAttributes.anchorPoint
		center.y += (layoutAttributes.anchorPoint.y - 0.5) * bounds.height
    }
}

 

2. UICollectionViewLayout

class CollectionViewLayout: UICollectionViewLayout {
    let itemSize = CGSize(width: 150, height: 150)
    var radius: CGFloat = 500 {
        didSet {
            invalidateLayout()
        }
    }
    
    var anglePerItem: CGFloat {
        return atan(itemSize.height / radius)
    }
    
    override var collectionViewContentSize: CGSize {
        return CGSize(width: CGFloat(collectionView!.numberOfItems(inSection: 0)) * itemSize.width,
                      height: CGRectGetHeight(collectionView!.bounds))


    }
    
    var attributesList = [CollectionViewLayoutAttributes]()
    override class var layoutAttributesClass: AnyClass {
        return CollectionViewLayoutAttributes.self
    }
    
    var angleAtExtreme: CGFloat {
        return collectionView!.numberOfItems(inSection: 0) > 0
        ? -CGFloat(collectionView!.numberOfItems(inSection: 0) - 1) * anglePerItem
        : 0
    }
    var angle: CGFloat {
        return angleAtExtreme * collectionView!.contentOffset.x / (collectionViewContentSize.width - CGRectGetWidth(collectionView!.bounds))
    }
    
    override func prepare() {
        super.prepare()

        let centerX = collectionView!.contentOffset.x + (CGRectGetWidth(collectionView!.bounds) / 2.0)
        let anchorPointY = ((itemSize.height / 2.0) + radius) / itemSize.height
        //1
        let theta = atan2(CGRectGetWidth(collectionView!.bounds) / 2.0, radius + (itemSize.height / 2.0) - (CGRectGetHeight(collectionView!.bounds) / 2.0))
        //2
        var startIndex = 0
        var endIndex = collectionView!.numberOfItems(inSection: 0) - 1
        //3
        if (angle < -theta) {
            startIndex = Int(floor((-theta - angle) / anglePerItem))
        }
        //4
        endIndex = min(endIndex, Int(ceil((theta - angle) / anglePerItem)))
        //5
        if (endIndex < startIndex) {
            endIndex = 0
            startIndex = 0
        }
        
        attributesList = (startIndex...endIndex).map { (i) -> CollectionViewLayoutAttributes in
            let attributes = CollectionViewLayoutAttributes(forCellWith: IndexPath(item: i, section: 0))
            attributes.size = self.itemSize
            attributes.center = CGPoint(x: centerX, y: CGRectGetMidY(self.collectionView!.bounds))
            attributes.angle = -(self.angle + (self.anglePerItem * CGFloat(i)))
            attributes.anchorPoint = CGPoint(x: 0.5, y: anchorPointY)

            return attributes
        }
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return attributesList
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return attributesList[safe: indexPath.row]
    }

    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        return true
    }
}

 

 

3. UICollectionViewLayoutAttributes

class CollectionViewLayoutAttributes: UICollectionViewLayoutAttributes {
    // 1
    var anchorPoint = CGPoint(x: 0.5, y: 0.5)
    var angle: CGFloat = 0 {
        // 2
        didSet {
            zIndex = Int(angle * 1000000)
            transform = CGAffineTransformMakeRotation(angle)
        }
    }
    // 3
    override func copy(with zone: NSZone? = nil) -> Any {
        let copiedAttributes: CollectionViewLayoutAttributes = super.copy(with: zone) as! CollectionViewLayoutAttributes
        copiedAttributes.anchorPoint = self.anchorPoint
        copiedAttributes.angle = self.angle
        return copiedAttributes
    }
}