NSAttributedString:将(多个)@username[userid] 提及转化为可点击的@username 链接

NSAttributedString: turn (multiple) @username[userid] mentions into clickable @username links

我正在编写代码以在 NSAttributedString 中显示提及,这需要 link 输出到用户个人资料。提及的格式是 @username[userid],需要显示为简单的 @username,这是可点击的。

到目前为止,我的代码工作正常,用户名变得可点击,但我现在需要删除 [userid] 部分,这当然会修改字符串的长度,以便范围不再匹配,等。不确定如何解决这个问题。

import Foundation
import UIKit

let comment = "Hello @kevin[1], @john and @andrew[2]!"

let wholeRange = NSRange(comment.startIndex..<comment.endIndex, in: comment)
let regex = try NSRegularExpression(pattern: #"(@[\w.-@]+)\[(\d+)\]"#)

let attributedString = NSMutableAttributedString(string: comment)

regex.enumerateMatches(in: comment, options: [], range: wholeRange) { match, _, _ in
  guard let match = match else {
    return
  }

  let userIdRange = Range(match.range(at: 2), in: comment)!
  let userId = comment[userIdRange]

  let usernameRange = match.range(at: 1)
  attributedString.addAttribute(NSAttributedString.Key.link, value: URL(string: "test://profile/\(userId)")!, range: usernameRange)
}

print(attributedString)

现在的结果可以这样表示,打印时:

Hello {
}@kevin{
    NSLink = "test://profile/1";
}[1], @john and {
}@andrew{
    NSLink = "test://profile/2";
}[2]!{
}

所以 @kevin@andrew 是 link,@john 不是(这是预期的!),但用户 ID 仍然可见。当然这是一个以前已经解决的问题,但我找不到任何例子,不知道要搜索什么关键字。有很多关于在字符串中检测 usernames/mentions 的问题,甚至更多关于在 NSAttributedString 中制作 link 的问题,但这不是我要解决的问题。

如何将 @username[userid] 提及变成可点击的 @username link,从而隐藏 [userid] 部分?

你只需要获取所有匹配的范围,以相反的顺序迭代它们,添加 link 然后用名称替换整个范围。类似于:

let comment = "Hello @kevin[1], @john and @andrew[2]!"
let attributedString = NSMutableAttributedString(string: comment)
let regex = try NSRegularExpression(pattern: #"(@[\w.-@]+)\[(\d+)\]"#)
var ranges: [(NSRange,NSRange,NSRange)] = []
regex.enumerateMatches(in: comment, range: NSRange(comment.startIndex..., in: comment)) { match, _, _ in
  guard let match = match else {
    return
  }
    ranges.append((match.range(at: 0),
                   match.range(at: 1),
                   match.range(at: 2)))
}

ranges.reversed().forEach {
    let userId = attributedString.attributedSubstring(from: [=10=].2).string
    let username = attributedString.attributedSubstring(from: [=10=].1).string
    
    attributedString.addAttribute(.link, value: URL(string: "test://profile/\(userId)")!, range: [=10=].0)
    attributedString.replaceCharacters(in: [=10=].0, with: username)
}
print(attributedString)

这将打印

Hello {
}@kevin{
NSLink = "test://profile/1";
}, @john and {
}@andrew{
NSLink = "test://profile/2";
}!{
}

很快完成:

let comment = "Hello @kevin[1], @john and @andrew[2]!"

let attributedString = NSMutableAttributedString(string: comment)

let wholeRange = NSRange(attributedString.string.startIndex..<attributedString.string.endIndex, in: attributedString.string)
let regex = try NSRegularExpression(pattern: #"(@[\w.-@]+)\[(\d+)\]"#)

let matches = regex.matches(in: attributedString.string, options: [], range: wholeRange)

matches.reversed().forEach { aResult in
    let fullMatchRange = Range(aResult.range(at: 0), in: attributedString.string)!      //@kevin[1]
    let replacementRange = Range(aResult.range(at: 1), in: attributedString.string)!    //@kevin
    let userIdRange = Range(aResult.range(at: 2), in: attributedString.string)!         // 1

    let atAuthor = String(attributedString.string[replacementRange])

    attributedString.addAttribute(.link,
                                  value: URL(string: "test://profile/\(attributedString.string[userIdRange])")!,
                                  range: NSRange(fullMatchRange, in: attributedString.string))
    attributedString.replaceCharacters(in: NSRange(fullMatchRange, in: attributedString.string),
                                       with: atAuthor)
}

print(attributedString)

输出:

Hello {
}@kevin{
    NSLink = "test://profile/1";
}, @john and {
}@andrew{
    NSLink = "test://profile/2";
}!{
}

看点:

  • 我换了图,方便抓拍。请参阅 forEach().

    评论中的示例
  • 我用了相反的顺序匹配,否则范围将不再准确!

  • 我一直在玩 attributedString.string 而不是 comment 以防它“不同步”。