3

I have a tableview cell containing a custom view among other views and autolayout is used.

The purpose of my custom view is to layout its subviews in rows and breaks into a new row if the current subview does not fit in the current line. It kind of works like a multiline label but with views. I achieved this through exact positioning instead of autolayout.

Since I only know the width of my view in layoutSubviews(), I need to calculate the exact positions and number of lines there. This worked out well, but the frame(zero) of my view didn't match because of missing intrinsicContentSize.

So I added a check to the end of my calculation if my height changed since the last layout pass. If it did I update the height property which is used in my intrinsicContentSize property and call invalidateIntrinsicContentSize().

I observed that initially layoutSubviews() is called twice. The first pass works well and the intrinsicContentSize is taken into account even though the width of the cell is smaller than it should be. The second pass uses the actual width and also updates the intrinsicContentSize. However the parent(contentView in tableview cell) ignores this new intrinsicContentSize.

So basically the result is that the subviews are layout and drawn correctly but the frame of the custom view is not updated/used in parent.

The question: Is there a way to notify the parent about the change of the intrinsic size or a designated place to update the size calculated in layoutSubviews() so the new size is used in the parent?

Edit:

Here is the code in my custom view.

FYI: 8 is just the vertical and horizontal space between two subviews

class WrapView : UIView {

    var height = CGFloat.zero

    override var intrinsicContentSize: CGSize {
        CGSize(width: UIView.noIntrinsicMetric, height: height)
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        guard frame.size.width != .zero else { return }

        // Make subviews calc their size
        subviews.forEach { $0.sizeToFit() }

        // Check if there is enough space in a row to fit at least one view
        guard subviews.map({ $0.frame.size.width }).max() ?? .zero <= frame.size.width else { return }

        let width = frame.size.width
        var row = [UIView]()

        // rem is the remaining space in the current row
        var rem = width
        var y: CGFloat = .zero
        var i = 0

        while i < subviews.count {
            let view = subviews[i]
            let sizeNeeded = view.frame.size.width + (row.isEmpty ? 0 : 8)
            let last = i == subviews.count - 1
            let fit = rem >= sizeNeeded

            if fit {
                row.append(view)
                rem -= sizeNeeded
                i += 1
                guard last else { continue }
            }

            let rowWidth = row.map { $0.frame.size.width + 8 }.reduce(-8, +)
            var x = (width - rowWidth) * 0.5

            for vw in row {
                vw.frame.origin = CGPoint(x: x, y: y)
                x += vw.frame.width + 8
            }

            y += row.map { $0.frame.size.height }.max()! + 8
            rem = width
            row = []
        }

        if height != y - 8 {
            height = y - 8
            invalidateIntrinsicContentSize()
        }
    }
}
Marco Seidel
  • 41
  • 1
  • 5
  • Hi, Marco. Adding some code to show what you've tried will go a long way towards getting an answer. – D. Greg Nov 14 '19 at 13:51
  • It would help to see a [mcve] ... but, one suggestion: instead of using `intrinsicContentSize`, try add a Height constraint var... at the end of you sizing loop, do `hConstraint.constant = y`. (No need for `invalidateIntrinsicContentSize()`) – DonMag Nov 14 '19 at 17:19
  • Whoops... make that `hConstraint.constant = y - 8` – DonMag Nov 14 '19 at 17:24
  • @DonMag I actually did that which worked for the custom view but caused effects on the other views in the cell like stretching labels bigger than the content and so on. So sadly this doesn’t work either. – Marco Seidel Nov 14 '19 at 17:30
  • @Marco - I have an explanation for ***why*** this is happening -- unfortunately I don't have a solution. Your code is updating the `intrinsicContentSize` *after* auto-layout has run its course... and changing the content of a cell does **not** automatically trigger a tableView redraw. You would probably need to send a message to the controller to run a `.beginUpdates()` / `.endUpdates()` pair on the table view, but you might end up in an infinite loop. – DonMag Nov 15 '19 at 17:15

1 Answers1

1

After a lot of trying and research I finally solved the bug.

As @DonMag mentioned in the comments the new size of the cell wasn't recognized until a new layout pass. This could be verified by scrolling the cell off-screen and back in which showed the correct layout. Unfortunately it is harder than expected to trigger new pass as .beginUpdates() + .endUpdates()didn't do the job.

Anyway I didn't find a way to trigger it but I followed the instructions described in this answer. Especially the part with the prototype cell for the height calculation provided a value which can be returned in tableview(heightForRowAt:).

Swift 5:

This is the code used for calculation:

let fitSize = CGSize(width: view.frame.size.width, height: .zero)
/* At this point populate the cell with the exact same data as the actual cell in the tableview */
cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()
cell.bounds = CGRect(x: .zero, y: .zero, width: view.frame.size.width, height: cell.bounds.height)
cell.setNeedsLayout()
cell.layoutIfNeeded()
height = headerCell.contentView.systemLayoutSizeFitting(fitSize).height + 1

The value is only calculated once and the cached as the size doesn't change anymore in my case.

Then the value can be returned in the delegate:

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    indexPath.row == 0 ? height : UITableView.automaticDimension
}

I only used for the first cell as it is my header cell and there is only one section.

Marco Seidel
  • 41
  • 1
  • 5
  • 1
    I have a similar problem, and fixed by another solution. The problem is "wrong cell height when the cell contains a custom view which calculates its height according to its width" https://stackoverflow.com/a/65788730/3658767 – fthdgn Jan 25 '21 at 08:08