I have the following function to find and highlight hashtags or mentions (@ or #) in a UILabel
:
class func addLinkAttribute(pattern: String,
toText text: String,
withAttributeName attributeName : String,
toAttributedString attributedString :NSMutableAttributedString,
withLinkAttributes linkAttributes: [NSObject : AnyObject]) {
var error: NSError?
if let regex = NSRegularExpression(pattern: pattern, options:.CaseInsensitive, error: &error) {
regex.enumerateMatchesInString(text, options: .allZeros, range: NSMakeRange(0, count(text))) { result, flags, stop in
let range = result.range
let start = advance(text.startIndex, range.location)
let end = advance(start, range.length)
let foundText = text.substringWithRange(Range<String.Index>(start: start,end: end))
var linkAttributesWithName = linkAttributes
linkAttributesWithName[attributeName] = foundText
attributedString.addAttributes(linkAttributesWithName, range: range)
}
}
}
If I pass a hashtag (#)(\\w+)
or mention (@)(\\w+)
pattern the code works perfectly but if the text contains an Emoji the range is offset by the number of emojis preceding it:
I know Swift treats strings differently to Objective-C, since count(string)
and count(string.utf16)
give me different results, but I am stumped as to how to account for this when using a regular expression.
I could just check the difference between the 2 counts and offset the range, but this seems wrong and hacky to me. There must be another way.
Similarly as in Swift extract regex matches, a possible solution is to convert the given Swift String
to an NSString
and apply the NSRange
s returned by
enumerateMatchesInString()
to that NSString
:
class func addLinkAttribute(pattern: String,
toText text: String,
withAttributeName attributeName : String,
toAttributedString attributedString :NSMutableAttributedString,
withLinkAttributes linkAttributes: [NSObject : AnyObject]) {
let nsText = text as NSString
var error: NSError?
if let regex = NSRegularExpression(pattern: pattern, options:.CaseInsensitive, error: &error) {
regex.enumerateMatchesInString(text, options: .allZeros, range: NSMakeRange(0, nsText.length)) {
result, _, _ in
let range = result.range
let foundText = nsText.substringWithRange(range)
var linkAttributesWithName = linkAttributes
linkAttributesWithName[attributeName] = foundText
attributedString.addAttributes(linkAttributesWithName, range: range)
}
}
}
(Alternative solution.) It is possible to convert an NSRange
to Range<String.Index>
without intermediate conversion to an NSString
.
With
extension String {
func rangeFromNSRange(nsRange : NSRange) -> Range<String.Index>? {
let utf16start = self.utf16.startIndex
if let from = String.Index(self.utf16.startIndex + nsRange.location, within: self),
let to = String.Index(self.utf16.startIndex + nsRange.location + nsRange.length, within: self) {
return from ..< to
}
return nil
}
}
from https://stackoverflow.com/a/30404532/1187415, your code can be written as
class func addLinkAttribute(pattern: String,
toText text: String,
withAttributeName attributeName : String,
toAttributedString attributedString :NSMutableAttributedString,
withLinkAttributes linkAttributes: [NSObject : AnyObject]) {
var error: NSError?
if let regex = NSRegularExpression(pattern: pattern, options:.CaseInsensitive, error: &error) {
regex.enumerateMatchesInString(text, options: .allZeros, range: NSMakeRange(0, count(text.utf16))) {
result, _, _ in
let nsRange = result.range
if let strRange = text.rangeFromNSRange(nsRange) {
let foundText = text.substringWithRange(strRange)
var linkAttributesWithName = linkAttributes
linkAttributesWithName[attributeName] = foundText
attributedString.addAttributes(linkAttributesWithName, range: nsRange)
}
}
}
}
and that should also work correctly for all kinds of extended grapheme clusters (Emojis, Regional Indicators etc...)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With