I am working on a project needs to add a UICollectionView(horizontal direction) inside UITableViewCell. The UITableViewCell height is using UITableViewAutoDimension and each UITableViewCell I am having a UIView(with a border for design requirements) as a base view, and in the UIView, I have a UIStackView added in as the containerView to proportionally fill the UICollectionView with two other buttons vertically. And then for UICollectionViewCell, I have added in a UIStackView to fill five labels.
Now, the auto-layout works if the UITableViewDelegate assigns a fixed height. But it doesn't work with the UITableViewAutoDimension. My guessing is that UICollectionView frame is not ready while UITableView is rendering its' cells. So the UITableViewAutoDimension calculates the UITableViewCell height with a default height of the UICollectionView which is zero.
So, of course, I have been searching before I throw a question out on the Internet but no solutions worked for me. Here are some links I have tried.
UICollectionView inside a UITableViewCell — dynamic height?
Making UITableView with embedded UICollectionView using UITableViewAutomaticDimension
UICollectionView inside UITableViewCell does NOT dynamically size correctly
Does anyone have the same issue? If the links above did work, please feel free to let me know in case I did it wrong. Thank you
--- Updated Sep 23th, 2018:
Layout Visualization: There are some UI modifications, but it does not change the issue that I am facing. Hope the picture can help.
- Code: The current code I have is actually not using the UIStackView in UITableViewCell and the UITableView heightForRowAtIndex I return a fixed height with 250.0. However, UITableView won't configure the cell height properly if I return UITableViewAutoDimension as I mentioned in my question.
1. UITableViewController
class ViewController: UIViewController {
private let tableView: UITableView = {
let tableView = UITableView()
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.register(ViewControllerTableViewCell.self, forCellReuseIdentifier: ViewControllerTableViewCell.identifier)
return tableView
}()
private lazy var viewModel: ViewControllerViewModel = {
return ViewControllerViewModel(models: [
Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 100, summary: "1% up"),
Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 200, summary: "2% up"),
Model(title: "TITLE", description: "SUBTITLE", currency: "USD", amount: 300, summary: "3% up"),
])
}()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
view.addSubview(tableView)
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return viewModel.numberOfSections
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.numberOfRowsInSection
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: ViewControllerTableViewCell.identifier, for: indexPath) as! ViewControllerTableViewCell
cell.configure(viewModel: viewModel.cellViewModel)
return cell
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 250.0
}
}
2. UITableViewCell
class ViewControllerTableViewCell: UITableViewCell {
static let identifier = "ViewControllerTableViewCell"
private var viewModel: ViewControllerTableViewCellViewModel!
private let borderView: UIView = {
let view = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.borderColor = UIColor.black.cgColor
view.layer.borderWidth = 1
return view
}()
private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.distribution = .fillProportionally
return stackView
}()
private let seperator: UIView = {
let seperator = UIView()
seperator.translatesAutoresizingMaskIntoConstraints = false
seperator.backgroundColor = .lightGray
return seperator
}()
private let actionButton: UIButton = {
let button = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.setTitle("Show business insight", for: .normal)
button.setTitleColor(.black, for: .normal)
return button
}()
private let pageControl: UIPageControl = {
let pageControl = UIPageControl()
pageControl.translatesAutoresizingMaskIntoConstraints = false
pageControl.pageIndicatorTintColor = .lightGray
pageControl.currentPageIndicatorTintColor = .black
pageControl.hidesForSinglePage = true
return pageControl
}()
private let collectionView: UICollectionView = {
let layout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: 200, height: 200), collectionViewLayout: layout)
collectionView.isPagingEnabled = true
collectionView.backgroundColor = .white
collectionView.showsHorizontalScrollIndicator = false
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.register(ViewControllerCollectionViewCell.self, forCellWithReuseIdentifier: ViewControllerCollectionViewCell.identifier)
return collectionView
}()
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setUpConstraints()
setUpUserInterface()
}
func configure(viewModel: ViewControllerTableViewCellViewModel) {
self.viewModel = viewModel
pageControl.numberOfPages = viewModel.items.count
collectionView.reloadData()
}
@objc func pageControlValueChanged() {
let indexPath = IndexPath(item: pageControl.currentPage, section: 0)
collectionView.scrollToItem(at: indexPath, at: .left, animated: true)
}
private func setUpConstraints() {
contentView.addSubview(borderView)
borderView.addSubview(actionButton)
borderView.addSubview(seperator)
borderView.addSubview(pageControl)
borderView.addSubview(collectionView)
NSLayoutConstraint.activate([
borderView.topAnchor.constraint(equalTo: topAnchor, constant: 10),
borderView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20),
borderView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20),
borderView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10),
])
NSLayoutConstraint.activate([
actionButton.heightAnchor.constraint(greaterThanOrEqualToConstant: 44),
actionButton.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
actionButton.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
actionButton.bottomAnchor.constraint(equalTo: borderView.bottomAnchor)
])
NSLayoutConstraint.activate([
seperator.heightAnchor.constraint(equalToConstant: 1),
seperator.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
seperator.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
seperator.bottomAnchor.constraint(equalTo: actionButton.topAnchor)
])
NSLayoutConstraint.activate([
pageControl.heightAnchor.constraint(greaterThanOrEqualToConstant: 40),
pageControl.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
pageControl.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
pageControl.bottomAnchor.constraint(equalTo: seperator.topAnchor)
])
NSLayoutConstraint.activate([
collectionView.topAnchor.constraint(equalTo: borderView.topAnchor),
collectionView.leadingAnchor.constraint(equalTo: borderView.leadingAnchor),
collectionView.trailingAnchor.constraint(equalTo: borderView.trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: pageControl.topAnchor)
])
}
private func setUpUserInterface() {
selectionStyle = .none
collectionView.delegate = self
collectionView.dataSource = self
pageControl.addTarget(self, action: #selector(pageControlValueChanged), for: .valueChanged)
}
}
extension ViewControllerTableViewCell: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return viewModel!.numberOfSections
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModel!.numberOfRowsInSection
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ViewControllerCollectionViewCell.identifier, for: indexPath) as! ViewControllerCollectionViewCell
let collectionCellViewModel = viewModel!.collectionCellViewModel(at: indexPath)
cell.configure(viewModel: collectionCellViewModel)
return cell
}
}
extension ViewControllerTableViewCell: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
debugPrint("did select \(indexPath.row)")
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
pageControl.currentPage = indexPath.row
}
}
extension ViewControllerTableViewCell: UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width - 40.0, height: collectionView.frame.height)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 20.0, bottom: 0, right: 20.0)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 40.0
}
}
3. UICollectionViewCell
class ViewControllerCollectionViewCell: UICollectionViewCell {
override class var requiresConstraintBasedLayout: Bool {
return true
}
static let identifier = "ViewControllerCollectionViewCell"
private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.distribution = .fillEqually
return stackView
}()
private let titleLabel: UILabel = {
let titleLabel = UILabel()
titleLabel.textColor = .black
titleLabel.font = UIFont.systemFont(ofSize: 20, weight: .bold)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
return titleLabel
}()
private let descriptionLabel: UILabel = {
let descriptionLabel = UILabel()
descriptionLabel.textAlignment = .right
descriptionLabel.textColor = .black
descriptionLabel.translatesAutoresizingMaskIntoConstraints = false
return descriptionLabel
}()
private let amountLabel: UILabel = {
let amountLabel = UILabel()
amountLabel.textColor = .black
amountLabel.textAlignment = .right
amountLabel.translatesAutoresizingMaskIntoConstraints = false
return amountLabel
}()
private let summaryLabel: UILabel = {
let summaryLabel = UILabel()
summaryLabel.textColor = .black
summaryLabel.textAlignment = .right
summaryLabel.translatesAutoresizingMaskIntoConstraints = false
return summaryLabel
}()
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(stackView)
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(descriptionLabel)
stackView.addArrangedSubview(amountLabel)
stackView.addArrangedSubview(summaryLabel)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: topAnchor),
stackView.leadingAnchor.constraint(equalTo: leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
stackView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(viewModel: CollectionCellViewModel) {
titleLabel.text = viewModel.title
descriptionLabel.text = viewModel.description
amountLabel.text = viewModel.amount.localizedCurrencyString(with: viewModel.currency)
summaryLabel.text = viewModel.summary
}
}
To enable self-sizing table view cells, you must set the table view’s rowHeight property to UITableViewAutomaticDimension. You must also assign a value to the estimatedRowHeight property, you also need an unbroken chain of constraints and views to fill the content view of the cell.
More: https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/WorkingwithSelf-SizingTableViewCells.html#//apple_ref/doc/uid/TP40010853-CH25-SW1
So in this case, the UIView with border determines the table view cell height dynamically, you have to tell the system how tall this UIView is, you can constraint this view to the UIStackView edges (top, bottom, leading, trailing), and use the UIStackView intrinsic content height as the UIView(with border) height.
The trouble is in the UIStackView intrinsic content height and with the distribution property. The UIStackView can estimate a height automatically for the two UIButtons (based on the size of text, the font style, and the font size), but the UICollectionView has no intrinsic content height, and since your UIStackview is filled proportionally the UIStackView sets a height of 0.0 for the UICollectionView, so it looks like something like this:
Fill Proportionally UIStackView
But if you change the UIStackView distribution property to fill equally you will have something like this:
Fill Equally UIStackView
If you want the UICollectionView to determine its own size and fill UIStackView proportionally, you need to set a height constraint for the UICollectionView. And then update the constraint based on the UICollectionViewCell content height.
Your code looks good, you only need to make minor changes, Here is a solution (updated 09/25/2018):
Also remember to set the tableView rowHeight to UITableView.automaticDimension & to give the system an estimated row height with: tableView.estimatedRowHeight.