Search code examples
iosswiftuicollectionviewuicollectionviewcelluicollectionviewflowlayout

UI CollectionView in UICollectionView Cell Programmatically


Hi I am trying to make a home feed like facebook using UICollectionView But in each cell i want to put another collectionView that have 3 cells.

you can clone the project here

I have two bugs the first is when i scroll on the inner collection View the bounce do not bring back the cell to center. when i created the collection view i enabled the paging and set the minimumLineSpacing to 0 i could not understand why this is happening. when i tried to debug I noticed that this bug stops when i remove this line

layout.estimatedItemSize = CGSize(width: cv.frame.width, height: 1)

but removing that line brings me this error

The behavior of the UICollectionViewFlowLayout is not defined because: the item height must be less than the height of the UICollectionView minus the section insets top and bottom values, minus the content insets top and bottom values

because my cell have a dynamic Height here is an example

example

my second problem is the text on each inner cell dosent display the good text i have to scroll until the last cell of the inner collection view to see the good text displayed here is an example example


Solution

  • You first issue will be solved by setting the minimumInteritemSpacing for the innerCollectionView in the OuterCell. So the definition for innerCollectionView becomes this:

    let innerCollectionView : UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.minimumLineSpacing = 0
        layout.minimumInteritemSpacing = 0
    
        let cv = UICollectionView(frame :.zero , collectionViewLayout: layout)
        cv.translatesAutoresizingMaskIntoConstraints = false
        cv.backgroundColor = .orange
        layout.estimatedItemSize =  CGSize(width: cv.frame.width, height: 1)
        cv.isPagingEnabled = true
        cv.showsHorizontalScrollIndicator = false
    
        return cv
    
    }()
    

    The second issue is solved by adding calls to reloadData and layoutIfNeeded in the didSet of the post property of OuterCell like this:

    var post: Post? {
        didSet {
            if let numLikes = post?.numLikes {
                likesLabel.text = "\(numLikes) Likes"
            }
    
            if  let numComments = post?.numComments {
                commentsLabel.text = "\(numComments) Comments"
            }
            innerCollectionView.reloadData()
            self.layoutIfNeeded()
        }
    }
    

    What you are seeing is related to cell reuse. You can see this in effect if you scroll to the yellow bordered text on the first item and then scroll down. You will see others are also on the yellow bordered text (although at least with the correct text now).

    EDIT

    As a bonus here is one method to remember the state of the cells.

    First you need to track when the position changes so in OuterCell.swft add a new protocol like this:

    protocol OuterCellProtocol: class {
        func changed(toPosition position: Int, cell: OutterCell)
    }
    

    then add an instance variable for a delegate of that protocol to the OuterCell class like this:

    public weak var delegate: OuterCellProtocol?
    

    then finally you need to add the following method which is called when the scrolling finishes, calculates the new position and calls the delegate method to let it know. Like this:

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        if let index = self.innerCollectionView.indexPathForItem(at: CGPoint(x: self.innerCollectionView.contentOffset.x + 1, y: self.innerCollectionView.contentOffset.y + 1)) {
            self.delegate?.changed(toPosition: index.row, cell: self)
        }
    }
    

    So that's each cell detecting when the collection view cell changes and informing a delegate. Let's see how to use that information.

    The OutterCellCollectionViewController is going to need to keep track the position for each cell in it's collection view and update them when they become visible.

    So first make the OutterCellCollectionViewController conform to the OuterCellProtocol so it is informed when one of its

    class OutterCellCollectionViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout, OuterCellProtocol   {
    

    then add a class instance variable to record the cell positions to OuterCellCollectionViewController like this:

    var positionForCell: [Int: Int] = [:]
    

    then add the required OuterCellProtocol method to record the cell position changes like this:

    func changed(toPosition position: Int, cell: OutterCell) {
        if let index = self.collectionView?.indexPath(for: cell) {
            self.positionForCell[index.row] = position
        }
    }
    

    and finally update the cellForItemAt method to set the delegate for a cell and to use the new cell positions like this:

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OutterCardCell", for: indexPath) as! OutterCell
        cell.post = posts[indexPath.row]
    
        cell.delegate = self
    
        let cellPosition = self.positionForCell[indexPath.row] ?? 0
        cell.innerCollectionView.scrollToItem(at: IndexPath(row: cellPosition, section: 0), at: .left, animated: false)
        print (cellPosition)
    
        return cell
    }
    

    If you managed to get that all setup correctly it should track the positions when you scroll up and down the list.