简体   繁体   中英

iOS - Custom view: Intrinsic content size ignored after updated in layoutSubviews()

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()
        }
    }
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM