简体   繁体   中英

UITableView + AutoLayout + Intrinsic Size: Where/when to calculate size?

I have create a custom view MyIntrincView which calculates its height automatically when setting its content. This works fine both in simulator and InterfaceBuilder.

However, when placing MyIntrinsicView inside a UITableViewCell , the cell height is not calculated correctly. Instead of automatically adopting the cell height to the intrinsic height of the view, all cell keep the same, initial height.

// A simple, custom view with intrinsic height. The height depends on
// the value of someProperty. When the property is changed setNeedsLayout
// is set and the height changes automatically.
@IBDesignable class MyIntrinsicView: UIView {
    @IBInspectable public var someProperty: Int = 5 {
        didSet { setNeedsLayout() }
    }
    override func layoutSubviews() {
        super.layoutSubviews()
        calcContent()
    }
    
    func calcContent() {
        height = CGFloat(20 * someProperty)
        invalidateIntrinsicContentSize()
    }
    
    
    @IBInspectable var height: CGFloat = 50
    override var intrinsicContentSize: CGSize {
        return CGSize(width: super.intrinsicContentSize.width, height: height)
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        invalidateIntrinsicContentSize()
    }
}


// A simple cell which only contains a MyIntrinsicView subview. The view
// is attached to trailing, leading, top and bottom anchors of the cell.
// Thus the cell height should automatically match the height of the
// MyIntrinsicView
class MyIntrinsicCell: UITableViewCell {
    @IBOutlet private var myIntrinsicView: MyIntrinsicView!
    
    var someProperty: Int {
        get { return myIntrinsicView.someProperty }
        set {
            myIntrinsicView.someProperty = newValue
            
            // Cell DOES NOT rezise without manualle calling layoutSubviews()
            myIntrinsicView.layoutSubviews()
        }
    }
}

...
// Simple tableView delegate which should create cells of different heights 
// by giving each cell a different someProperty 
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "IntrinsicCell", for: indexPath) as? MyIntrinsicCell ?? MyIntrinsicCell()
    
    // Give all cell a different intrinsic height by setting someProperty to rowIndex
    cell.someProperty = indexPath.row
    return cell
}

I would expect, that each cell has a different height ( 20 * someProperty = 20 * indexPath.row ). However, instead all cell have the same, initial height.

Only when explicitly calling myIntrinsicView.layoutSubviews() the cells are created with the correct height.

It seems that the tableView does not call myIntrinsicView.layoutSubviews() . Why is this?

When using a UILabel instad of MyIntrinsicView as cell content with different text lengths, everything works as expected. Thus the overall tableView setup is correct (= cell sizes are calculated automatically) and there has to be way to use intrinsic sizes correctly in UITableView as well. So, what exactly is the correct way to do this?

As with your previous question here I think you're not really understanding what intrinsicContentSize does and does not do.

When setting the text of a UILabel , yes, its intrinsicContentSize changes, but that's not all that happens.

You also don't want to do what you're trying inside layoutSubviews() ... if your code does trigger a change, you'll get into an infinite recursion loop (again, as with your previous question).

Take a look at modifications to your code:

// A simple, custom view with intrinsic height. The height depends on
// the value of someProperty. When the property is changed setNeedsLayout
// is set and the height changes automatically.
@IBDesignable class MyIntrinsicView: UIView {
    @IBInspectable public var someProperty: Int = 5 {
        didSet {
            calcContent()
            setNeedsLayout()
        }
    }
    
    func calcContent() {
        height = CGFloat(20 * someProperty)
        invalidateIntrinsicContentSize()
    }
    
    
    @IBInspectable var height: CGFloat = 50
    override var intrinsicContentSize: CGSize {
        return CGSize(width: super.intrinsicContentSize.width, height: height)
    }
    
    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()
        invalidateIntrinsicContentSize()
    }
}


// A simple cell which only contains a MyIntrinsicView subview. The view
// is attached to trailing, leading, top and bottom anchors of the cell.
// Thus the cell height should automatically match the height of the
// MyIntrinsicView
class MyIntrinsicCell: UITableViewCell {
    @IBOutlet private var myIntrinsicView: MyIntrinsicView!
    
    var someProperty: Int {
        get { return myIntrinsicView.someProperty }
        set {
            myIntrinsicView.someProperty = newValue
        }
    }
}

Two side notes...

1 - Your posted code shows you calling myIntrinsicView.layoutSubviews() ... from Apple's docs:

You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.

2 - For the direction it looks like you're headed, you would probably be better off manipulating constraint constants, rather than intrinsic content size.

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