Search code examples
iosswiftscrolluicollectionview

Creating a custom collectionView that overlays the cells


I am playing around with collectionViews,
trying to create a custom design for image layout.
See below:

enter image description here

I have achieved this.
I know the code isn't the most beautiful or regular, however I am intrigued to see if this is possible.
When I scroll, the cell are pushed to the bottom of the screen, to where they are no longer visible.

Is there a fix for this?

Results after scrolling vvv

enter image description here

enter image description here

Here is the code: It can be copied to a blank project and run by itself for testing.

Any input would be greatly appreciated.

Thanks

import UIKit

class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    
    
    var spiralNumber = 0
    var collectionView: UICollectionView!

    
    let colors = [UIColor.yellow, UIColor.red, UIColor.blue, UIColor.green, UIColor.purple, UIColor.systemRed, UIColor.systemMint, UIColor.systemBlue, UIColor.cyan, UIColor.gray, UIColor.systemTeal, UIColor.systemYellow]///testing purposes
    
    func generateRandomNumber(min: Int, max: Int) -> Int {
        return Int.random(in: min...max)
    }//testing purposes
    
    func subtractionFive(number: Int) -> Int {
        let largestMultipleOfFive = (number / 5) * 5
        return number - largestMultipleOfFive
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        let layout = UICollectionViewFlowLayout()
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = -(view.frame.width * 2/3) / 2
        layout.sectionInset = .zero
  
        collectionView = UICollectionView(frame: view.frame, collectionViewLayout: layout)
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.backgroundColor = .white
        view.addSubview(collectionView)
        
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 300
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        
        let randomNumber = generateRandomNumber(min: 0, max: colors.count-1)
        let randomColor = colors[randomNumber]
        cell.backgroundColor = randomColor
  
        let spiralIndex = subtractionFive(number: indexPath.row)
        spiralNumber = spiralIndex
        let rowHeight = collectionView.frame.width
        let rowNumber = CGFloat((indexPath.row/5))
        let rowOffset = rowNumber*rowHeight

        
        print("rowNumber-->", Int(rowNumber), rowOffset, rowOffset/rowNumber, spiralNumber)
        
        if spiralNumber == 0 {
            cell.frame = CGRect(x: 0, y: 0+rowOffset, width: cell.frame.width, height: cell.frame.height)
            
        }else if spiralNumber == 1{
            cell.frame = CGRect(x: cell.frame.width*2, y: 0+rowOffset, width: cell.frame.width, height: cell.frame.height)
            
        }else if spiralNumber == 2{
            cell.frame = CGRect(x: 0, y: (cell.frame.height/2)+rowOffset, width: cell.frame.width, height: cell.frame.height)
            
        } else if spiralNumber == 3 {
            cell.frame = CGRect(x: cell.frame.width, y: cell.frame.height+rowOffset, width: cell.frame.width, height: cell.frame.height)
        } else if spiralNumber == 4{
            cell.frame = CGRect(x: cell.frame.width/2, y: (cell.frame.height*2)+rowOffset, width: cell.frame.width, height: cell.frame.height)
        }else{
            cell.frame = CGRect(x: cell.frame.width, y: (cell.frame.height*2)+rowOffset, width: cell.frame.width, height: cell.frame.height)
        }
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = collectionView.frame.width
        let spiralIndex = subtractionFive(number: indexPath.row)
        spiralNumber = spiralIndex
        
        if spiralNumber == 0 || spiralNumber == 4{
            return CGSize(width: width * 2/3, height: width * 1/3)
        }else if spiralNumber == 1 || spiralNumber == 2{
            return CGSize(width: width * 1/3, height: width * 2/3)
        }else if spiralNumber == 3{
            return CGSize(width: width * 1/3, height: width * 1/3)
        }else{
            return CGSize(width: width * 2/3, height: width * 1/3)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        print("test->", indexPath.row/5)
    }
    
}

I was expecting the cells to remain in place as I scroll down. I.e. Normal behaviour of a collectionView.


Solution

  • First be careful don't do any resizing in this function

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath)
    

    all resizing should be in here

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)
    

    Second Collection view does not know how to draw cells inside of it so it needs a layout so apple provide us with a default layout called UICollectionViewFlowLayout. which you implement sizeForItemAt function to tell the layout the size you want for each cell but it's limited so when you want to do something like your example you need to ignore the default layout and create your own custom layout here is the final code

    import UIKit
    
    class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
    
    
    var spiralNumber = 0
    var collectionView: UICollectionView!
    
    
    let colors = [UIColor.yellow, UIColor.red, UIColor.blue, UIColor.green, UIColor.purple, UIColor.systemRed, UIColor.systemMint, UIColor.systemBlue, UIColor.cyan, UIColor.gray, UIColor.systemTeal, UIColor.systemYellow]///testing purposes
    
    func generateRandomNumber(min: Int, max: Int) -> Int {
        return Int.random(in: min...max)
    }//testing purposes
    
    func subtractionFive(number: Int) -> Int {
        let largestMultipleOfFive = (number / 5) * 5
        return number - largestMultipleOfFive
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let layout = CustomCollectionViewLayout()
        
    
        collectionView = UICollectionView(frame: view.frame, collectionViewLayout: layout)
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        collectionView.backgroundColor = .white
        view.addSubview(collectionView)
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return 1
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 300
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
        
        let randomNumber = generateRandomNumber(min: 0, max: colors.count-1)
        let randomColor = colors[randomNumber]
        cell.backgroundColor = randomColor
    
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    //        print("test->", indexPath.row/5)
        }
        
    }
    
    
    class CustomCollectionViewLayout: UICollectionViewLayout {
        
    private var cachedAttributes: [UICollectionViewLayoutAttributes] = []
    private var contentHeight:CGFloat = 0
    var spiralNumber = 0
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else {
            return 0
        }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }
    
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
    
    func subtractionFive(number: Int) -> Int {
        let largestMultipleOfFive = (number / 5) * 5
        return number - largestMultipleOfFive
    }
    
    override func prepare() {
        guard cachedAttributes.isEmpty, let collectionView else {return}
    
        for index in 0..<collectionView.numberOfItems(inSection: 0) {
            let indexPath = IndexPath(item: index, section: 0)
    
            let spiralIndex = subtractionFive(number: indexPath.row)
            spiralNumber = spiralIndex
            let rowHeight = contentWidth
            let rowNumber = CGFloat((indexPath.row/5))
            let rowOffset = rowNumber*rowHeight
    
    
            let width = contentWidth
            spiralNumber = spiralIndex
            var size: CGSize = .zero
    
            if spiralNumber == 0 || spiralNumber == 4{
                size =  CGSize(width: width * 2/3, height: width * 1/3)
            }else if spiralNumber == 1 || spiralNumber == 2{
                size =  CGSize(width: width * 1/3, height: width * 2/3)
            }else if spiralNumber == 3{
                size =  CGSize(width: width * 1/3, height: width * 1/3)
            }else{
                size =  CGSize(width: width * 2/3, height: width * 1/3)
            }
    
            var frame:CGRect = .zero
    
            if spiralNumber == 0 {
                frame = CGRect(x: 0, y: 0+rowOffset, width: size.width, height: size.height)
    
            }else if spiralNumber == 1{
                frame = CGRect(x: size.width*2, y: 0+rowOffset, width: size.width, height: size.height)
    
            }else if spiralNumber == 2{
                frame = CGRect(x: 0, y: (size.height/2)+rowOffset, width: size.width, height: size.height)
    
            } else if spiralNumber == 3 {
                frame = CGRect(x: size.width, y: size.height+rowOffset, width: size.width, height: size.height)
            } else if spiralNumber == 4{
                frame = CGRect(x: size.width/2, y: (size.height*2)+rowOffset, width: size.width, height: size.height)
            }else{
                frame = CGRect(x: size.width, y: (size.height*2)+rowOffset, width: size.width, height: size.height)
            }
    
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = frame
            cachedAttributes.append(attributes)
            contentHeight = max(contentHeight, frame.maxY)
    
        }
    
    }
    
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var visibleLayoutAttributes: [UICollectionViewLayoutAttributes] = []
        for attributes in cachedAttributes {
            if attributes.frame.intersects(rect) {
                visibleLayoutAttributes.append(attributes)
            }
        }
        
        return visibleLayoutAttributes
     }
    }
    

    Finally if you want to learn more about collection view custom layout check this helpful blog https://www.kodeco.com/4829472-uicollectionview-custom-layout-tutorial-pinterest