关于 swift:Color 属性在 NSAttributedString 和 NSLinkAttributeName 中被忽略

Color attribute is ignored in NSAttributedString with NSLinkAttributeName

NSAttributedString 中,一系列字母具有链接属性和自定义颜色属性。

在带有 Swift 2 的 Xcode 7 中,它可以工作:

enter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Cocoa
import XCPlayground

let text ="Hey @user!"

let attr = NSMutableAttributedString(string: text)
let range = NSRange(location: 4, length: 5)
attr.addAttribute(NSForegroundColorAttributeName, value: NSColor.orangeColor(), range: range)
attr.addAttribute(NSLinkAttributeName, value:"http://somesite.com/", range: range)

let tf = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 50))
tf.allowsEditingTextAttributes = true
tf.selectable = true
tf.stringValue = text
tf.attributedStringValue = attr

XCPlaygroundPage.currentPage.liveView = tf

Swift 3,Xcode 8:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import Cocoa
import PlaygroundSupport

let text ="Hey @user!"

let attr = NSMutableAttributedString(string: text)
let range = NSRange(location: 4, length: 5)
attr.addAttribute(NSForegroundColorAttributeName, value: NSColor.orange, range: range)
attr.addAttribute(NSLinkAttributeName, value:"http://somesite.com/", range: range)

let tf = NSTextField(frame: NSRect(x: 0, y: 0, width: 200, height: 50))
tf.allowsEditingTextAttributes = true
tf.isSelectable = true
tf.stringValue = text
tf.attributedStringValue = attr

PlaygroundPage.current.liveView = tf

我已经向 Apple 发送了错误报告,但与此同时,如果有人对 Xcode 8 中的修复或解决方法有想法,那就太好了。


苹果开发者已经回答:

Please know that our engineering team has determined that this issue behaves as intended based on the information provided.

他们解释了为什么它以前有效但不再有效:

Unfortunately, the previous behavior (attributed string ranges with NSLinkAttributeName rendering in a custom color) was not explicitly supported. It happened to work because NSTextField was only rendering the link when the field editor was present; without the field editor, we fall back to the color specified by NSForegroundColorAttributeName.

Version 10.12 updated NSLayoutManager and NSTextField to render links using the default link appearance, similar to iOS. (see AppKit release notes for 10.12.)

To promote consistency, the intended behavior is for ranges that represent links (specified via NSLinkAttributeName) to be drawn using the default link appearance. So the current behavior is the expected behavior.

(强调我的)


此答案不能解决 NSLinkAttributeName 忽略自定义颜色的问题,它是在 NSAttributedString 中使用彩色可点击单词的替代解决方案。

通过这种解决方法,我们根本不使用 NSLinkAttributeName,因为它会强制使用我们不想要的样式。

相反,我们使用自定义属性,并且子类化 NSTextField/NSTextView 来检测鼠标点击下的属性并采取相应的行动。

显然有几个限制:您必须能够子类化字段/视图、覆盖 mouseDown 等,但在等待修复时"它适用于我"。

在准备 NSMutableAttributedString 时,您将在其中设置 NSLinkAttributeName,将链接设置为具有自定义键的属性:

1
2
3
theAttributedString.addAttribute("CUSTOM", value: theLink, range: theLinkRange)
theAttributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.orange, range: theLinkRange)
theAttributedString.addAttribute(NSCursorAttributeName, value: NSCursor.arrow(), range: theLinkRange)

设置了链接的颜色和内容。现在我们必须让它可点击。

为此,将您的 NSTextView 子类化并覆盖 mouseDown(with event: NSEvent)

我们将获取鼠标事件在窗口中的位置,在该位置找到文本视图中的字符索引,并在文本视图的属性字符串中询问该索引处字符的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyTextView: NSTextView {

    override func mouseDown(with event: NSEvent) {
        // the location of the click event in the window
        let point = self.convert(event.locationInWindow, from: nil)
        // the index of the character in the view at this location
        let charIndex = self.characterIndexForInsertion(at: point)
        // if we are not outside the string...
        if charIndex < super.attributedString().length {
            // ask for the attributes of the character at this location
            let attributes = super.attributedString().attributes(at: charIndex, effectiveRange: nil)
            // if the attributes contain our key, we have our link
            if let link = attributes["CUSTOM"] as? String {
                // open the link, or send it via delegate/notification
            }
        }
        // cascade the event to super (optional)
        super.mouseDown(with: event)
    }

}

就是这样。

在我的例子中,我需要用不同的颜色和链接类型来定制不同的词,所以不是只将链接作为字符串传递,而是传递一个包含链接和其他元信息的结构,但想法是一样的。

如果您必须使用 NSTextField 而不是 NSTextView,则查找点击事件位置有点棘手。一个解决方案是在 NSTextField 中创建一个 NSTextView 并从那里使用与以前相同的技术。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyTextField: NSTextField {

    var referenceView: NSTextView {
        let theRect = self.cell!.titleRect(forBounds: self.bounds)
        let tv = NSTextView(frame: theRect)
        tv.textStorage!.setAttributedString(self.attributedStringValue)
        return tv
    }

    override func mouseDown(with event: NSEvent) {
        let point = self.convert(event.locationInWindow, from: nil)
        let charIndex = referenceView.textContainer!.textView!.characterIndexForInsertion(at: point)
        if charIndex < self.attributedStringValue.length {
            let attributes = self.attributedStringValue.attributes(at: charIndex, effectiveRange: nil)
            if let link = attributes["CUSTOM"] as? String {
                // ...
            }
        }
        super.mouseDown(with: event)
    }

}