0
votes

I'm trying to build something

I'm trying to build a tag list view using UICollectionView and nest it into my custom UITableViewCell.

enter image description here

What do I have now

After searching the internet, I find the key to the problem:

  • Subclass UICollectionView and implement it's intrinsic content size property.

However, when I nest my custom UICollectionView into a self-sizing UITableViewCell, the whole thing doesn't work well. The layout is broken.

No matter how do I change the code, I get one of the following 3 buggy UIs.

enter image description here

The height of the collection view is always wrong, either too small or too large, it can not hug it's content just right.

When I use Debug View Hierarchy to check the views, I find that although the UI is broken, the contentSize property of the collection view has a correct value. It seems that the content size property can not be reflected to the UI in time.

class IntrinsicCollectionView: UICollectionView {
    
    override var contentSize: CGSize {
        didSet {
            invalidateIntrinsicContentSize()
        }
    }
    
    override var intrinsicContentSize: CGSize {
        layoutIfNeeded()
        return CGSize(width: UIView.noIntrinsicMetric, height: collectionViewLayout.collectionViewContentSize.height)
    }
    
    override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
        super.init(frame: frame, collectionViewLayout: layout)
        isScrollEnabled = false
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

There are many solution about how to create a custom UICollectionView with intrinsic content size. Some of them can work correctly. But when nesting them into a UITableViewCell, none of them works well.

There are also some answer about just nest one UICollectionView into UITableViewCell without other views. But if there are also some UILabel in UITableViewCell, it won't work.

I upload all the code to github. https://github.com/yunhao/nest-collectionview-in-tableviewcell

Thank you!

1

1 Answers

0
votes

I'll try to explain what's going on....

To make it easy to understand, in your ListViewController let's work with just one row to begin with:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return 1 // items.count
}

In your ListViewCell class, add these lines at the end of prepareViews():

    // so we can see the element frames
    titleLabel.backgroundColor = .green
    subtitleLabel.backgroundColor = .cyan
    collectionView.backgroundColor = .yellow

In your IntrinsicCollectionView class, let's add a print() statement to give us some information:

override var intrinsicContentSize: CGSize {
    layoutIfNeeded()
    
    // add this line
    print("collView Width:", frame.width, "intrinsic height:", collectionViewLayout.collectionViewContentSize.height)
    
    return CGSize(width: UIView.noIntrinsicMetric, height: collectionViewLayout.collectionViewContentSize.height)
}

When I then run the app on an iPhone 8, I get this result:

enter image description here

and I see this in the debug console:

collView Width: 66.0 intrinsic height: 350.0
collView Width: 343.0 intrinsic height: 30.0

What that tells me is that the collection view is asked for its intrinsicContentSize before it has a complete frame.

At that point, it fills in its cells, and its layout ends up with a .collectionViewContentSize.height of 350 (this row has six "tags" cells).

Auto-layout then performs another pass... the collection view now has a valid frame width (based on the cell width)... and the cells are re-laid-out.

Unfortunately, the table view has already set the row height(s), based on the initial collection view intrinsicContentSize.height.

So, two steps that may (should) fix this:

In ListViewCell, invalidate the content size of the collection view when you get the tags:

func setTags(_ tags: [String]) {
    self.tags = tags
    collectionView.reloadData()
    
    // add this line
    collectionView.invalidateIntrinsicContentSize()
}

Then, in ListViewController, we need to reload the table after its frame has changed:

// add this var
var currentWidth: CGFloat = 0

// implement viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    if view.frame.width != currentWidth {
        currentWidth = view.frame.width
        tableView.reloadData()
    }
}

That seems (with very quick testing) to give me reliable results:

enter image description here enter image description here

and on device rotation:

enter image description here