0

My goal is to create a layout presented below:

The layout

I know how to create these custom UICollectionViewCells, but I trouble with the layout. All of the shown cells differ in width, so there can be, for instance: four in the first row, two in the second, and the last one remaining - in the third. That's just one possible configuration, and there are many more (including one shown on an image) depending on the label's width.

I'm building everything programmatically. Also I feel like using UICollectionView is the best choice, but I'm open to any suggestions.

Thanks in advance!

What I've already tried:

let collectionView = UICollectionView(frame: .zero, collectionViewLayout: TagLayout()

override func viewDidLoad() {
    super.viewDidLoad()
    setupCollectionView()
}

private func setupCollectionView() {
    collectionView.backgroundColor = .systemGray5
    collectionView.dataSource = self
    collectionView.delegate = self
    collectionView.register(SubjectCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
        
    //adding a view to subview and constraining it programmatically using Stevia
}

extension ProfileVC: UICollectionViewDelegate, UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 5
    }
    
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? SubjectCollectionViewCell else { return UICollectionViewCell() }
        cell.data = SubjectTagData(emoji: "", subjectName: "Item I")
        
        return cell
    }
}
wictorious
  • 851
  • 3
  • 10
  • 24

1 Answers1

2

Use following collectionViewLayout

    // MARK: - TagLayoutDelegate
    protocol TagLayoutDelegate: class {
        func widthForItem(at indexPath: IndexPath) -> CGFloat
        func rowHeight() -> CGFloat
    }

    // MARK: - TagLayout
    class TagLayout: UICollectionViewLayout {

    // MARK: Variables
    weak var delegate       : TagLayoutDelegate?
    var cellPadding         : CGFloat = 5.0
    var deafultRowHeight    : CGFloat = 35.0
    var scrollDirection     : UICollectionView.ScrollDirection = .vertical
    
    private var contentWidth: CGFloat = 0
    private var contentHeight: CGFloat = 0
    
    private var cache: [UICollectionViewLayoutAttributes] = []
    
    // MARK: Public Functions
    func reset() {
        cache.removeAll()
        contentHeight = 0
        contentWidth = 0
    }
    
    // MARK: Override
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
    
    override func prepare() {
        super.prepare()
        if scrollDirection == .vertical {
            prepareForVerticalScroll()
        }
        else {
            prepareForHorizontalScroll()
        }
    }
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        var visibleLayoutAttributeElements: [UICollectionViewLayoutAttributes] = []
        for attribute in cache {
            if attribute.frame.intersects(rect) {
                visibleLayoutAttributeElements.append(attribute)
            }
        }
        return visibleLayoutAttributeElements
    }
    
    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache[indexPath.item]
    }
    
    // MARK: Private Functions
    private func prepareForVerticalScroll() {
        guard cache.isEmpty, let collectionView = collectionView else {
            return
        }
        
        let noOfItems   = collectionView.numberOfItems(inSection: 0)
        var xOffset     = [CGFloat](repeating: 0.0, count: noOfItems)
        var yOffset     = [CGFloat](repeating: 0.0, count: noOfItems)
        
        let insets      = collectionView.contentInset
        contentWidth = collectionView.bounds.width - (insets.left + insets.right)
        
        var rowWidth: CGFloat = 0
        for i in 0 ..< noOfItems {
            let indexPath = IndexPath(item: i, section: 0)
            let textWidth = delegate?.widthForItem(at: indexPath) ?? 75.0
            let width = textWidth + cellPadding
            let height = delegate?.rowHeight() ?? 30.0
            let frame = CGRect(
                x: xOffset[i],
                y: yOffset[i],
                width: width,
                height: height
            )
            let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
            
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            cache.append(attributes)
            
            contentHeight = max(contentHeight, frame.maxY)
            
            
            rowWidth += frame.width
            
            if i < noOfItems-1 {
                let nextIP = IndexPath(item: i+1, section: 0)
                let nextWidth = delegate?.widthForItem(at: nextIP) ?? 75.0
                if rowWidth + nextWidth + cellPadding <= contentWidth {
                    xOffset[i+1] = xOffset[i] + width
                    yOffset[i+1] = yOffset[i]
                }
                else {
                    rowWidth = 0
                    yOffset[i+1] = yOffset[i] + (delegate?.rowHeight() ?? 30.0)
                }
            }
        }
    }
    
    private func prepareForHorizontalScroll() {
        guard cache.isEmpty, let collectionView = collectionView else {
            return
        }
        
        let insets = collectionView.contentInset
        contentHeight = collectionView.bounds.height - (insets.top + insets.bottom)
        
        let rowHeight = delegate?.rowHeight() ?? deafultRowHeight
        
        let noOfRows: Int = 2
        
        var yOffset: [CGFloat] = []
        for row in 0 ..< noOfRows {
            yOffset.append(CGFloat(row) * rowHeight)
        }
        
        var row = 0
        var xOffset: [CGFloat] = [CGFloat](repeating: 0.0, count: noOfRows)
        
        for i in 0 ..< collectionView.numberOfItems(inSection: 0) {
            let indexPath = IndexPath(item: i, section: 0)
            let textWidth = delegate?.widthForItem(at: indexPath) ?? 75.0
            let width = textWidth + cellPadding
            let frame = CGRect(
                x       : xOffset[row],
                y       : yOffset[row],
                width   : width,
                height  : rowHeight)
            let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
            
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            cache.append(attributes)
            
            contentWidth = max(contentWidth, frame.maxX)
            xOffset[row] = xOffset[row] + width
            
            row = row < (noOfRows - 1) ? row + 1 : 0
        }
     }
   }

And implement it as follows

let tagLayout = TagLayout()
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: tagLayout)

private func setupCollectionView() {
  tagLayout.delegate = self
  //Your other code goes here
}

extension ProfileVC: TagLayoutDelegate {
  func widthForItem(at indexPath: IndexPath) -> CGFloat {
    return 100.0
  }
  func rowHeight() -> CGFloat {
    return 30.0
  }
}
udbhateja
  • 948
  • 6
  • 21
  • First of all, thanks a lot for your answer. I have probably done something not quite right because cells pop up stacked almost on top of each other. I can provide a screenshot if you want, but I did everything right when it comes to setting up `UICollectionView` programmatically. Also I'm not looking for a scrollable behavior for this one, I just want them to look as presented in the image. – wictorious Oct 29 '20 at 18:58
  • 1
    Ok share the code and if you don't want the scrolling behaviour, set `collectionView. isScrollEnabled = false` – udbhateja Oct 30 '20 at 07:25
  • updated question with code. – wictorious Oct 31 '20 at 09:06
  • Updated the answer, please check. @wictorious – udbhateja Oct 31 '20 at 09:18
  • Brilliant! It works smoothly with couple of adjustments. Thank you so much! – wictorious Oct 31 '20 at 10:26
  • The only issue left afaik is prob caused by me, but want to double check. Do you think first cells in the each row being cut from left is caused by your Layout or badly configured cell? – wictorious Oct 31 '20 at 10:58
  • 2
    ok. found out a bug i made. it works as expected now. :) – wictorious Oct 31 '20 at 12:24