在基于视图的 NSTableView 中,NSTextField 被 NSImageView 裁剪

In a view-based NSTableView, the NSTextField is clipped by an NSImageView

长话短说:

在 macOS 10.11 中,基于视图的 NSTableView 在文本字段下方包含一个 NSTextField 和一个 NSImageView,其中一些行有图像而其他行没有,一些文本在 table 向下滚动然后向上滚动后被剪裁.

滚动前:

滚动后:

当我说 "clipped" 时,这意味着文本视图处于自动布局定义的最小高度,此时应该展开它。

请注意,此错误行为仅发生在 macOS 10.11 Mavericks 中。 macOS 10.12 Sierra 不存在此类问题。


上下文

macOS 10.11+,基于视图 NSTableView。

每一行都有一个文本框,文本框下方有一个图像视图。

所有元素都在 IB 中设置了自动布局约束。

目标

文本视图必须垂直调整其高度 - 图像视图应相应移动,并保持粘在文本字段的底部。

当然,行本身也必须调整其高度。

有时没有图像可显示,在这种情况下图像视图不应该可见。

正常工作时应该是这样的:

当前实施

该行是 NSTableCellView 的子类。

在单元格中,文本字段设置有属性字符串。

tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat 中,我制作了一个虚拟 textView,用于查找实际文本字段的高度,并 return 相应地修改了行高。我还检查是否有要显示的图像,如果是,我将图像高度添加到行高。

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

tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?中我准备了实际的单元格内容:

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

问题

滚动 table 视图时,一旦图像视图中的一行或多行有图像,就会出现显示问题。

剪辑文本

有时文本会被剪裁,可能是因为空图像视图遮住了文本的底部:

正常

剪辑

重要提示:调整 table 的大小会触发重绘并修复显示问题...

我试过的

小区重用

我认为主要问题是因为 tableView 重用了单元格。

所以我在 tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? 中默认隐藏图像字段,只有在有图像要设置时才取消隐藏,我在主线程上执行此操作:

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

但在某些情况下,imageView 仍然会阻塞文本视图。

无论如何,有时在第一次显示时文本会在滚动完成之前被剪裁...

核心动画层

我认为单元格可能需要分层支持才能正确重新显示,所以我在滚动视图、table视图、table单元格上启用了 CoreAnimation 层,等。但我几乎看不出有什么区别。

A/B 测试

如果我完全删除 imageView 并只处理文本字段,一切正常。拥有 imageView 绝对是这里出现问题的原因。

自动布局

我试过在不使用自动布局的情况下处理行内容的显示,但无济于事。我也尝试过设置不同的约束条件,但显然我不喜欢自动布局,而且没能找到一个很好的替代方案来替代我当前的约束条件。

备选方案:带有图像的 NSAttributedString

我尝试删除图像视图,并将图像添加到文本视图中属性字符串的末尾。但是当它工作时,结果通常是丑陋的——而且在大多数情况下它只是不起作用(例如,当图像不能及时下载以添加到属性字符串时,使单元格根本没有文本或图像而且高度不对)。

问题

我做错了什么?我该如何解决这些问题?我的猜测是我应该更改自动布局约束,但我没有看到可行的解决方案。

或者您会采用完全不同的方式吗?

文本字段在 IB 中有 "Preferred Width" 字段,它允许调整固有大小与 auto-layout 计算大小的相关性。

将此 IB 属性 值更改为 "First Runtime Layout Width" 或 "Explicit" 通常有助于解决类似问题。

这解决了我过去的很多问题。