Search code examples
iosswiftuitableviewuikitlayoutsubviews

UITableViewCell hides part of content after appearing on the screen with custom layoutSubviews()


I'm struggling with UITableView. As you can see in this video in third section of table view third cell isn't display correctly. That happens when I dequeue my cell like that:

let cell = tableView.dequeueReusableCell(withIdentifier: MultipleSelectAnswerSurveyTableViewCellIdentifier, for: indexPath) as! MultipleSelectAnswerSurveyTableViewCell
cell.setup(answer: question.answers?[indexPath.row].value ?? "", isSelected: false, style: style, isLastInSection: indexPath.row == (question.answers?.count ?? 1) - 1)
return cell

Cell's setup() method:

func setup(answer: String, isSelected: Bool, style: Style, isLastInSection: Bool) {
    self.isLastInSection = isLastInSection
    selectionStyle = .none
    backgroundColor = style.survey.singleSelectAnswerTableViewCell.backgroundColor
    answerLabel.textColor = style.survey.singleSelectAnswerTableViewCell.answerLabelColor
    answerLabel.font = style.survey.singleSelectAnswerTableViewCell.answerLabelFont
    answerLabel.text = answer
    addSubview(answerLabel)
    addSubview(selectionIndicator)
    answerLabel.snp.makeConstraints { make in
        make.left.equalTo(8)
        make.centerY.equalTo(selectionIndicator.snp.centerY)
        make.top.equalTo(8)
        make.bottom.equalTo(-8)
        make.right.equalTo(selectionIndicator.snp.left).offset(-8)
    }
    selectionIndicator.snp.makeConstraints { make in
        make.right.equalTo(-8)
        make.top.greaterThanOrEqualTo(8)
        make.bottom.lessThanOrEqualTo(-8)
        make.width.height.equalTo(26)
    }
}

self.isLastInSection variable is used inside layoutSubviews():

override func layoutSubviews() {
    super.layoutSubviews()
    if isLastInSection {
        roundCorners(corners: [.bottomLeft, .bottomRight], radius: 16.0)
    }
    contentView.layoutIfNeeded()
}

And finally roundCorners():

extension UIView {
    func roundCorners(corners: UIRectCorner, radius: CGFloat) {
        let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
        let mask = CAShapeLayer()
        mask.path = path.cgPath
        layer.mask = mask
    }
}

When I dequeue cell with isLastInSection set to false cell is being displayed as expected (related video). So I think the problem is in life cycle of the cell and when the layoutSubview() is being called. I tried many solutions for similar problem found in different threads but none of them helped me. tableView(_:heightForRowAt:) causes the third cell to display correctly, but the first one has rounded bottom corners. Also all of them are fixed in height and that cannot happen. But what is really weird: when I print the isLastInSection during dequeueing cell which is unexpectedly rounded debugger returns me false:

(lldb) po indexPath.row == (question.answers?.count ?? 1) - 1 false

enter image description here

As you can see in Debug View Hierarchy view text exists so that's why I 've defined the problem as hiding part of content.

Debug View hierarchy


Solution

  • You dequeue cell and each time you add subviews you don't check if they are already there which will happen in case of recycled cell. That probably breaks constraints and causes incorrect sizing.

    Same problem with rounding - you set rounded corners, but you never revert this behavior when reused cell should not be rounded.

    Best way to solve this issue would be to add additional check and create subviews only once:

    func setup(answer: String, isSelected: Bool, style: Style, isLastInSection: Bool) {
        if self.subviews.count == 0 {
            // adding subviews etc.
            // code that needs to be performed only once for whole cell's life
        }
    
        self.isLastInSection = isLastInSection
        // set up content that changes for each cell (like text)
        // for example a code depending on parameters of this method
    }
    

    alternatively you could keep some property like isInitialized and check that at the beginning.

    Also your method layoutSubviews must support both cases:

    override func layoutSubviews() {
        super.layoutSubviews()
        if isLastInSection {
            roundCorners(corners: [.bottomLeft, .bottomRight], radius: 16.0)
        } else {
            layer.mask = nil
        }
        contentView.layoutIfNeeded()
    }