Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I highlight text in a string that contains emojis in Swift?

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:

enter image description here

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.

like image 414
Michael Gaylord Avatar asked May 22 '15 15:05

Michael Gaylord


1 Answers

Similarly as in Swift extract regex matches, a possible solution is to convert the given Swift String to an NSString and apply the NSRanges 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...)

like image 127
Martin R Avatar answered Sep 20 '22 16:09

Martin R