简体   繁体   中英

Autolayout support for custom text view

This question is about supporting a variable-height, custom text view using constraints and the view's intrinsicContentSize for autolayout. Before you click that 'duplicate' button, hear me out.

I have a custom text view (from scratch, inherits from NSView). It supports many of the usual NSTextView features, the most relevant here being multiple lines and laying out its content based on width available. The app it's built for loads a couple of these text views into each row of a table view. The issue is that the height doesn't get set properly according to its intrinsicContentSize.

I created a sample project that simplifies the problem. It uses a "pseudo" text view with a fixed number and size of characters used to determine width/height required. In the sample, there is a table view of one column whose cell view has only one subview, a PseudoTextView. The text view is pinned to the edges of its cell view with a little padding. How can I get the system to recognize that the text view should abide by the constraints that define the width while allowing the text view to grow in height, wrapped tightly by the cell view? Here's the text view code:

class PseudoTextView: NSView {
@IBInspectable var characterCount: Int = 0
@IBInspectable var characterWidth: CGFloat = 5
@IBInspectable var characterHeight: CGFloat = 8
@IBInspectable var background: NSColor = .blue {
    didSet {
        layer?.backgroundColor = background.cgColor
    }
}

required init?(coder decoder: NSCoder) {
    super.init(coder: decoder)
    wantsLayer = true
    layer?.backgroundColor = background.cgColor
}

override var intrinsicContentSize: NSSize {
    let requiredWidth = characterWidth * CGFloat(characterCount)
    let lineCount = (requiredWidth / frame.width).rounded(.up)
    let usedHeight = lineCount * characterHeight
    let charactersPerLine = (frame.width / characterWidth).rounded(.down)
    let usedWidth = charactersPerLine * characterWidth
    return NSSize(width: usedWidth, height: usedHeight)
}

This version returns the appropriate size based on the frame of the view. This obviously doesn't work because it's accessed during the updateConstraints phase of layout when the frame hasn't been set. I've also tried using NSView.noIntrinsicMetric for the width, but this will drive the text view to zero width and the height never recovers. There are an enormous number of other attempts I've made, but I won't bore you with them all.

NSTextField does something different (assuming 'Editable' is off, 'Wrap' is on). It's intrinsicContentSize reports the full width of the text on a single line (even if it's much longer than the width available), but it is somehow resized to the correct width. Once resized, the intrinsicContentWidth then still reports the full single-line width, but the height is adjusted to account for multiple lines. There is some magic somewhere I haven't been able to divine.

I've read every line of related documentation. If there's a blog post on the topic, I've probably read it. If there's a question on SO on the topic, I've probably read it. If you wrote a book on the topic, I've probably bought it. All of these sources tease at the problem I'm having, but none of them answer the question of how to handle this particular situation. Desperate.

Update:

After reading an old blog post by Jonathon Mah ( http://devetc.org/code/2014/07/07/auto-layout-and-views-that-wrap.html ) I created an example that uses his approach. Here's another project that mimics his technique and works correctly. This is the top portion of the app. It's a fixed container view that's adjusted with a slider. The patchwork are the pseudo characters of the custom view whose background is the pink color.

在此处输入图片说明

However, when inserted into a self-sizing table view, the custom view correctly matches the width of its cell, but the cell is not adjusted to respect the intrinsic height. If you change the custom view's bottom constraint to be optional (say, with a >= relation) the custom view does shrink to the correct height, but the cell view remains fixed. How do I convince the cell view to shrink its height to respect the intrinsicContentSize.height of its subview?

在此处输入图片说明

I have a solution for your problem, although it may not be optimal, since I do not have too much experience with macos specifics. So, first of all, let's define that the table view should use automatic row heights:

override func awakeFromNib() {
    super.awakeFromNib()
    tableView.usesAutomaticRowHeights = true
}

In your second sample the tableView outlet was not not connected to TableViewController , but it probably should be, so do not forget to connect it.

Now, in your WrappingCellView , you override layout() , but the value that you set for preferredMaxLayoutWidth is incorrect. I think it should be the width of the superview:

override func layout() {
    // 16 is used for the sake of example
    wrappingView.preferredMaxLayoutWidth = (superview?.frame.width ?? 16) - 16
    super.layout()
}

Next part is the one I am not sure about:

func tableViewColumnDidResize(_ notification: Notification) {
    tableView.reloadData()
}

There should be a better API to recalculate row heights. I hope you or someone else can suggest something :)

These three adjustments result in proper recalculation of the cell height: 在此处输入图片说明

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