NSAttributedString 和表情符号:位置和长度问题

NSAttributedString and emojis: issue with positions and lengths

我正在使用 NSAttributedString 为来自 API(想想 Twitter 上的“@mention”)的文本的某些部分着色。

API 为我提供了文本和一组实体,这些实体表示文本中提到的部分(或链接、标签等),这些部分应该是彩色的。

但有时,颜色会因为表情符号而偏移。


例如,使用此文本:

"@ericd Some text. @apero"

API 给出:

[ { "text" : "ericd", "len" : 6, "pos" : 0 }, { "text" : "apero", "len" : 6, "pos" : 18 } ]

我使用 NSRange 成功地将其转换为 NSAttributedString:

for m in entities.mentions {
    let r = NSMakeRange(m.pos, m.len)
    myAttributedString.addAttribute(NSForegroundColorAttributeName, value: someValue, range: r)
}

我们看到 "pos": 18 是正确的,这是“@apero”开始的地方。正如预期的那样,彩色部分是“@ericd”和“@apero”。

但是当文本中使用了一些特定的表情符号组合时,API 不能很好地转换为 NSATtributedString,着色为 offset:

"@ericd Some text. ✌ @apero"

给出:

[ { "text" : "ericd", "len" : 6, "pos" : 0 }, { "text" : "apero", "len" : 6, "pos" : 22 } ]

"pos": 22:API作者说的是对的,我理解他们的观点。

不幸的是,NSAttributedString 不同意,我的着色是关闭的:

第二次提及的最后一个字符没有着色(因为 "pos" 因为表情符号太短了?)。

您可能已经猜到了,我无法以任何方式改变 API 的行为方式,我必须在客户端进行调整。

除了...我不知道该做什么。我是否应该尝试检测文本中的表情符号类型并在有问题的表情符号时手动修改提及的位置?但是,检测哪些表情符号会移动位置而哪些不会移动的标准是什么?以及如何决定我需要多少偏移量?也许问题是由 NSAttributedString 引起的?

我知道这与表情符号的长度有关,与它们作为离散字符的长度相比,但是……好吧……我迷路了(叹息)。


请注意,我已尝试实施类似于 this stuff 的解决方案,因为我的 API 与此兼容,但它仅部分起作用,一些表情符号仍在破坏索引:

A Swift String 在其内容上提供了不同的 "views"。 Swift 博客中的 "Strings in Swift 2" 给出了很好的概述:

  • characters 是字符值的集合,或扩展的字素簇。
  • unicodeScalars 是 Unicode 标量值的集合。
  • utf8是UTF–8编码单元的集合。
  • utf16 是 UTF–16 编码单元的集合。

在讨论中,poslen 来自您的 API 是 Unicode 标量视图的索引。

另一方面,NSMutableAttributedStringaddAttribute()方法取一个NSRange,即对应的范围 NSString.

中 UTF-16 代码点的索引

String 提供了在索引之间 "translate" 的方法 不同观点(比较NSRange to Range<String.Index>):

let text = "@ericd Some text. ✌ @apero"
let pos = 22
let len = 6

// Compute String.UnicodeScalarView indices for first and last position:
let from32 = text.unicodeScalars.index(text.unicodeScalars.startIndex, offsetBy: pos)
let to32 = text.unicodeScalars.index(from32, offsetBy: len)

// Convert to String.UTF16View indices:
let from16 = from32.samePosition(in: text.utf16)
let to16 = to32.samePosition(in: text.utf16)

// Convert to NSRange by computing the integer distances:
let nsRange = NSRange(location: text.utf16.distance(from: text.utf16.startIndex, to: from16),
                      length: text.utf16.distance(from: from16, to: to16))

这个 NSRange 是您需要的属性字符串:

let attrString = NSMutableAttributedString(string: text)
attrString.addAttribute(NSForegroundColorAttributeName,
                        value: UIColor.red,
                        range: nsRange)

Swift4(Xcode9)的更新:在Swift4,标准库 提供在 Swift String 范围和 NSString 之间转换的方法 范围,因此计算简化为

let text = "@ericd Some text. ✌ @apero"
let pos = 22
let len = 6

// Compute String.UnicodeScalarView indices for first and last position:
let fromIdx = text.unicodeScalars.index(text.unicodeScalars.startIndex, offsetBy: pos)
let toIdx = text.unicodeScalars.index(fromIdx, offsetBy: len)

// Compute corresponding NSRange:
let nsRange = NSRange(fromIdx..<toIdx, in: text)