1
votes

TL;DR:

In macOS 10.11, a view based NSTableView containing an NSTextField and an NSImageView right under the textfield, with some rows having an image and others not, some texts are clipped after having scrolled the table down then up.

Before scrolling:

enter image description here

After scrolling:

enter image description here

When I say "clipped" it means that the text view is at its minimum height as defined with autolayout, when it should be expanded instead.

Note that this faulty behavior only happens in macOS 10.11 Mavericks. There's no such issues with macOS 10.12 Sierra.


Context

macOS 10.11+, view based NSTableView.

Each row has a textfield, and an image view just below the textfield.

All elements have autolayout constraints set in IB.

enter image description here

enter image description here

Goal

The text view has to adapt its height vertically - the image view should move accordingly, staying glued to the bottom of the textfield.

Of course the row itself also has to adapt its height.

Sometimes there's no image to display, in this case the image view should not be visible.

This is how it's supposed to render when it works properly:

enter image description here

Current implementation

The row is a subclass of NSTableCellView.

In the cell the textfield is set with an attributed string.

In tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat, I make a dummy textView that I use to find the actual textfield's height, and return an amended row height accordingly. I also check if there's an image to display and if it's the case I add the image height to the row height.

let ns = NSTextField(frame: NSRect(x: 0, y: 0, width: defaultWidth, height: 0))
ns.attributedStringValue = // the attributed string
var h = ns.attributedStringValue.boundingRect(with: ns.bounds.size, options: [.usesLineFragmentOrigin, .usesFontLeading]).height
if post.hasImage {
    h += image.height
}
return h + margins

In tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? I prepare the actual cell contents:

getUserAvatar { img in
    DispatchQueue.main.async {
        cell.iconBackground.layer?.backgroundColor = self.prefs.colors.iconBackground.cgColor
        cell.iconBackground.rounded(amount: 6)
        cell.iconView.rounded(amount: 6)
        cell.iconView.image = img
    }    
}

cell.usernameLabel.attributedStringValue = xxx
cell.dateLabel.attributedStringValue = xxx

// the textView at the top of the cell
cell.textLabel.attributedStringValue = xxx

// the imageView right under the textView
if post.hasImage {
    getImage { img in
        DispatchQueue.main.async {
            cell.postImage.image = img
        }
    }
}

Issues

When scrolling the tableView, there's display issues as soon as one or several rows have an image in the image view.

Clipped text

Sometimes the text is clipped, probably because the empty image view is masking the bottom part of the text:

Normal

enter image description here

Clipped

enter image description here

IMPORTANT: resizing the table triggers a redraw and fixes the display issue...

What I've tried

Cell reuse

I thought the main issue was because of cell reuse by the tableView.

So I'm hiding the image field by default in tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?, only unhiding if there's an image to set, and I do it on the main thread:

guard let cell = tableView.make(withIdentifier: "xxx", owner: self) as? PostTableCellView else {
    return nil
}
DispatchQueue.main.async {
    cell.postImage.isHidden = true
    cell.postImage.image = nil
}
// set the text view here, the buttons, labels, etc, then this async part runs:
downloadImage { img in
    DispatchQueue.main.async {
        cell.postImage.isHidden = false
        cell.postImage.image = img
    }   
}
// more setup
return cell

But the imageView is still blocking the text view in some cases.

And anyway, sometimes the text is clipped at the first display, before any scrolling is done...

CoreAnimation Layer

I thought maybe the cells need to be layer backed so that they're correctly redisplayed, so I've enabled CoreAnimation layer on the scrollView, the tableView, the tableCell, etc. But I hardly see any difference.

A/B test

If I remove the imageView completely and only deal with the textfield everything works ok. Having the imageView is definitely what is causing issues here.

Autolayout

I've tried to handle the display of the row contents without using autolayout but to no avail. I've also tried to set different constraints but clearly I suck at autolayout and didn't manage to find a good alternative to my current constraints.

Alternative: NSAttributedString with image

I've tried to remove the image view and have the image added at the end of the attributed string in the text view instead. But the result is often ugly when it works - and for most times it just doesn't work (for example when the image can't be downloaded in time to be added to the attributed string, leaving the cell without text or image at all and at a wrong height).

Question

What am I doing wrong? How could I fix these issues? My guess is that I should change the autolayout constraints but I don't see a working solution.

Or maybe would you do this entirely differently?

1
You're talking about NSTextView and in the pictures and code you're using NSTextField. Did you check if ns.bounds.size is correct in heightOfRow?Willeke
Arf, my fault, I've changed from NSTextView to NSTextField some time ago while testing, and forgot to revert back. Do you think using NSTextField is wrong and I should go back to NSTextView? // Woah, ns.bounds.size always has zero height... Not sure what's happening, is it because of NSTextField instead of NSTextView?Eric Aya
I just discovered that this works without issues in macOS 10.12 Sierra. The wrong behavior happens only in macOS 10.11 Mavericks... oOEric Aya
There is a "Preferred Width" for your text field. You IB screen shot show you have it set Automatic. Did you play with other values? Both in IB or runtime. Also did you play with noteHeightOfRowsWithIndexesChanged. Autolayout may need call you delegate again.Eugene Mankovski
Some people call it auto-layout magic. Others treat it as auto-layout drama. There is really not much to add in here. I had similar issues in a few places in my app and it fixed the issue. I think it is related to the order of how auto-layout calculates constraints and the fact that text field has intrinsic size.Eugene Mankovski

1 Answers

1
votes

Text field has "Preferred Width" field in IB which allows tune up correlation of intrinsic size with auto-layout calculated size.

Changing this IB property value to "First Runtime Layout Width" or "Explicit" often helps resolving similar issues.

enter image description here

This resolved a lot of my issues in past.