NSAttributedString和emojis:位置和长度问题

aya*_*aio 13 unicode nsattributedstring emoji nsrange swift

我正在使用NSAttributedString为来自API的文本的某些部分(在Twitter上认为"@mention")着色.

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

但有时,由于表情符号,颜色会被抵消.


例如,使用此文本:

"@ericd一些文字.@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)
}
Run Code Online (Sandbox Code Playgroud)

我们认为这"pos": 18是正确的,这是"@apero"开始的地方.正如预期的那样,彩色部分是"@ericd"和"@apero".

但是当在文本中使用表情符号的某些特定组合时,API不能很好地转换为NSATtributedString,颜色是偏移的:

"@ericd一些文字.✌@apero"

得到:

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

"pos": 22:API作者声明这是正确的,我理解他们的观点.

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

在此输入图像描述

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

正如您可能已经猜到的那样,我无论如何都无法改变API的行为方式,我必须在客户端进行调整.

除此之外......我不知道该怎么做.我是否应该尝试检测文本中的表情符号,并在有问题的表情符号时手动修改提及的位置?但是,检测哪个表情符号改变位置以及哪个表情符号没有改变的标准是什么?以及如何确定我需要多少偏移?也许问题是由NSAttributedString引起的?

我知道这与表情符号的长度相比,与他们作为离散字符的长度相比,但是......嗯......我迷失了(叹气).


请注意,我已经尝试实现类似于这个东西的解决方案,因为我的API与此兼容,但它只是部分工作,一些表情符号仍在破坏索引:

在此输入图像描述

Mar*_*n R 19

Swift String对其内容提供了不同的"视图".Swift博客中的"Strings in Swift 2"中给出了一个很好的概述:

  • characters 是字符值或扩展字形集群的集合.
  • unicodeScalars 是Unicode标量值的集合.
  • utf8 是UTF-8代码单元的集合.
  • utf16 是UTF-16代码单元的集合.

正如在讨论中发现的那样,pos并且len从您的API是Unicode标量视图的索引.

在另一方面,所述addAttribute()的方法NSMutableAttributedString需要一个NSRange,即,对应于在所述UTF-16代码点的索引的范围内NSString.

String提供在不同视图的索引之间"转换"的方法(将NSRange与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))
Run Code Online (Sandbox Code Playgroud)

NSRange是属性字符串所需的内容:

let attrString = NSMutableAttributedString(string: text)
attrString.addAttribute(NSForegroundColorAttributeName,
                        value: UIColor.red,
                        range: nsRange)
Run Code Online (Sandbox Code Playgroud)

更新Swift 4(Xcode 9):在Swift 4中,标准库提供了在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)
Run Code Online (Sandbox Code Playgroud)