UICollectionViewCell improperly sizes itself after calling reloadData()

I have a UICollectionView in my ViewController. This collectionView has a dynamic height so depending on how much text is inside the collectionView it will resize the height.

This works perfectly fine I can click all the buttons, scroll up or down, etc, but the problem is that when I call reloadData(), the collectionViewCells stack on top of each other and i'm not sure why.

Here is a picture of the collectionView before reloadData() is called:

[https://ibb.co/fSmM9q3) (Sorry it was giving me an error for this image so here is a link for it)

Here is the picture after I call reloadData():

Here is my Custom Collection VIew Code:

     
  public let bottomRefresh = CollectionViewBottomRefresh()

  init() {
    let layout = UICollectionViewFlowLayout()
    layout.minimumLineSpacing = 0
    layout.minimumInteritemSpacing = 0
    layout.scrollDirection = .vertical
    layout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 50)
    super.init(frame: .zero, collectionViewLayout: layout)
    alwaysBounceVertical = true
    backgroundColor = .systemBackground
    delaysContentTouches = false
    showsVerticalScrollIndicator = false
    register(PostView.self, forCellWithReuseIdentifier: "post")
  }
   
  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
   
  override func touchesShouldCancel(in view: UIView) -> Bool {
    if view is UIButton || view is UITextField {
      return true
    }
    return super.touchesShouldCancel(in: view)
  }
}

Here is my Collection View Cell Code:

   
  // Sets a requried width and a dynamic height that changes depending on what is in the cell. So we can have searchbar as first cell heigh 50, and post in other cells with height of view.bounds.width.
  override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)
    layoutAttributes.frame.size = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
    return layoutAttributes
  }
   
  private func addConstraints() {
    kuduAppTeamDeleteButton.height(30)
    kuduAppTeamDeleteButton.width(UIScreen.main.bounds.width / 4)
    kuduAppTeamDeleteButton.bottom(to: commentsButton)
    kuduAppTeamDeleteButton.leftToRight(of: commentsButton, offset: 5)
     
    imageViewButton.width(UIScreen.main.bounds.width)
    imageViewButton.height(UIScreen.main.bounds.width * 9/16)
    imageViewButton.topToSuperview()
     
    infoButton.leftToRight(of: titleLabel, offset: 6)
    infoButton.topToBottom(of: imageViewButton, offset: 15)
    infoButton.width(30)
    infoButton.height(30)
     
    titleLabel.horizontalToSuperview(insets: UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 40))
    titleLabel.topToBottom(of: imageViewButton, offset: 5)
    titleLabel.height(min: 50)
     
    likeButton.topToBottom(of: titleLabel, offset: 10)
    likeButton.trailingToSuperview(offset: 10)
    likeButton.height(32)
    likeButton.width(32)
     
    profile.leadingToSuperview(offset: 5)
    profile.topToBottom(of: titleLabel, offset: 10)
    profile.widthToSuperview(multiplier: 0.4)
     
    likeCount.trailingToLeading(of: likeButton, offset: -5)
    likeCount.topToBottom(of: titleLabel, offset: 15)
     
    followButton.topToBottom(of: titleLabel, offset: 5)
    followButton.trailingToLeading(of: likeCount, offset: -10)
    followButton.height(50)
    followButton.width(UIScreen.main.bounds.width / 4)
     
    date.bottom(to: commentsButton, offset: -5)
    date.trailingToSuperview(offset: 5)
     
    commentsButton.topToBottom(of: profile, offset: 10)
    commentsButton.leadingToSuperview(offset: 5)
     
    line.horizontalToSuperview()
    line.bottom(to: commentsButton)
    line.height(1)
     
    contentView.bottom(to: line)
    contentView.widthToSuperview()
  }

Thanks in advance!

Answered by Trevdawg347 in 699446022

Finally figured this out

You have to use a UITableView instead of a UICollectionView so you can access the variable UITableView.automaticDimension. You also need to wrap the cell contents inside a UIView so the cells can automatically size themselves. Without this UIView they don't size themselves for some reason.

Side note: You can only use UITableView if you have one column.

Here is my tableView with self sizing / dynamic height:

class MyTableView: UITableView {

    
    public let bottomRefresh = TableViewBottomRefresh()


    init() {

        super.init(frame: .zero, style: .plain)

        rowHeight = UITableView.automaticDimension

        estimatedRowHeight = 500

        separatorStyle = .none

        allowsSelection = false

        delaysContentTouches = false

        alwaysBounceVertical = true

        showsVerticalScrollIndicator = false

        register(MyTableViewCell.self, forCellReuseIdentifier: "MyCell")

    }

Here is my UIView so the cells can size themselves correctly:

    private let containerView: UIView = {

       let view = UIView()

        view.translatesAutoresizingMaskIntoConstraints = false

        return view

    }()

need to add contentView.addsubview(containerView) to your cell and then put all the cell contents inside your containerView:

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {

        super.init(style: style, reuseIdentifier: reuseIdentifier)

        contentView.addSubview(containerView)

        containerView.addSubview(profileImage)

        containerView.addSubview(username)

        containerView.addSubview(editProfileButton)

        addConstraints()

    }

Here are my constraints (note: I am using TinyConstraints SDK):

   private func addConstraints() {

        profileImage.topToSuperview()

        profileImage.centerXToSuperview()

        profileImage.widthToSuperview(multiplier: 1/2)

        profileImage.height(UIScreen.main.bounds.width / 2)

        

        username.topToBottom(of: profileImage, offset: 10)

        username.horizontalToSuperview()

        username.height(50)

        

        editProfileButton.height(50)

        editProfileButton.widthToSuperview(multiplier: 1/3)

        editProfileButton.centerXToSuperview()

        editProfileButton.topToBottom(of: username, offset: 10)

        

        containerView.widthToSuperview()

        containerView.bottom(to: editProfileButton)

    }

Hope this helps! If anyone ever figures out how to call reloadData() with self sizing collectionView cells let me know! I tried for 2 weeks and couldn't figure it out :(.

collectionViewCells stack on top of each other

A probable reason is that your cellForItemAt causes the problem.

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

For instance, if you add subviews in cell, you have to remove first any existing subview in the cell and add new ones.

So, please show the code of the func to confirm.

Another point. Why do you specify a zero height ?

    let targetSize = CGSize(width: layoutAttributes.frame.width, height: 0)

It seems your cell height should be at least 40 ; so why not specify 40 ?

Accepted Answer

Finally figured this out

You have to use a UITableView instead of a UICollectionView so you can access the variable UITableView.automaticDimension. You also need to wrap the cell contents inside a UIView so the cells can automatically size themselves. Without this UIView they don't size themselves for some reason.

Side note: You can only use UITableView if you have one column.

Here is my tableView with self sizing / dynamic height:

class MyTableView: UITableView {

    
    public let bottomRefresh = TableViewBottomRefresh()


    init() {

        super.init(frame: .zero, style: .plain)

        rowHeight = UITableView.automaticDimension

        estimatedRowHeight = 500

        separatorStyle = .none

        allowsSelection = false

        delaysContentTouches = false

        alwaysBounceVertical = true

        showsVerticalScrollIndicator = false

        register(MyTableViewCell.self, forCellReuseIdentifier: "MyCell")

    }

Here is my UIView so the cells can size themselves correctly:

    private let containerView: UIView = {

       let view = UIView()

        view.translatesAutoresizingMaskIntoConstraints = false

        return view

    }()

need to add contentView.addsubview(containerView) to your cell and then put all the cell contents inside your containerView:

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {

        super.init(style: style, reuseIdentifier: reuseIdentifier)

        contentView.addSubview(containerView)

        containerView.addSubview(profileImage)

        containerView.addSubview(username)

        containerView.addSubview(editProfileButton)

        addConstraints()

    }

Here are my constraints (note: I am using TinyConstraints SDK):

   private func addConstraints() {

        profileImage.topToSuperview()

        profileImage.centerXToSuperview()

        profileImage.widthToSuperview(multiplier: 1/2)

        profileImage.height(UIScreen.main.bounds.width / 2)

        

        username.topToBottom(of: profileImage, offset: 10)

        username.horizontalToSuperview()

        username.height(50)

        

        editProfileButton.height(50)

        editProfileButton.widthToSuperview(multiplier: 1/3)

        editProfileButton.centerXToSuperview()

        editProfileButton.topToBottom(of: username, offset: 10)

        

        containerView.widthToSuperview()

        containerView.bottom(to: editProfileButton)

    }

Hope this helps! If anyone ever figures out how to call reloadData() with self sizing collectionView cells let me know! I tried for 2 weeks and couldn't figure it out :(.

UICollectionViewCell improperly sizes itself after calling reloadData()
 
 
Q