问题
I have a label where I have normal text and hashtags. I wanted the hashtags to have a different color, so I did something like this:
let text = "#hellothere I am you. #hashtag #hashtag2 @you #hashtag3"
let words = text.components(separatedBy: " ")
let attribute = NSMutableAttributedString.init(string: text)
for word in words {
let range = (text as NSString).range(of: word)
if word.hasPrefix("#") {
attribute.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: range)
}
}
label.attributedText = attribute
However, when i am using this logic, the results are inconsistent. Sometimes, the hashtags get colored as expected, other times some portion of the word containing the hashtag gets colored along with some portion of the next word. I am looking for a solution where I can identify if there is a "#" character in the word (the first occurrence in the word only) and then color the word from the hashtag till the next whitespace, comma, semicolon, full stop or end of string, whichever may be applicable.
回答1:
This calls for a regular expression. Example in Swift 3.
let attrStr = NSMutableAttributedString(string: "#hellothere I am you. #hashtag, #hashtag2 @you #hashtag3")
let searchPattern = "#\\w+"
var ranges: [NSRange] = [NSRange]()
let regex = try! NSRegularExpression(pattern: searchPattern, options: [])
ranges = regex.matches(in: attrStr.string, options: [], range: NSMakeRange(0, attrStr.string.characters.count)).map {$0.range}
for range in ranges {
attrStr.addAttribute(NSForegroundColorAttributeName, value: UIColor.yellow, range: NSRange(location: range.location, length: range.length))
}
attrStr
Paste the above into a playground and look at the output. To see the regex in action try this link.
回答2:
I am not able to test this right now, but a thought came to mind;
when you search for the string #hashtag
in your example-string, don't you think it will find three locations, and that this can have something to do with it? Perhaps not that particular example-string, but let's say you have the string "#hellothere #hashtag2 #hashtag #hashtag3"
, and perform your code.
When you loop to the string "#hashtag"
and use text.range(of:"#hashtag")
, it will first find the string "#hashtag2"
. I'm not sure if it will continue looking for multiple matches, but with your code, "#hashtag2"
is equally correct, as it contains "#hashtag"
.
I don't know the best way to fix this, but I have a few ideas.
One idea might be to somehow store the letter before and after the word you are looking for. So that when you use text.range(of:string)
, you're not only looking for "#hashtag"
, but looking for " #hashtag "
.
It will not be enough to just add a space before and after the string in question, as it could've been separated by another sign (comma or period etc.).
A better solution may be to sum up the previous parts of the string, and "manually" calculate the range of your string in question, instead of using text.range(of:string)
.
Not real code, but try to understand what I'm thinking:
let text = "#hellothere I am you. #hashtag #hashtag2 @you #hashtag3"
let words = text.components(separatedBy: " ")
let attribute = NSMutableAttributedString.init(string: text)
for (index, word) in words.enumerated() {
if word.hasPrefix("#") {
//Recreate the original string up to this part, so you can find the actual start-index of your current word.
let sentenceBeforeWord = words[0..<index].joined(separator: " ")
let range = //
attribute.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: range)
}
}
label.attributedText = attribute
Then calculate the range of your word (#hashtag) yourself. Now you know exactly when your word will start (its startIndex should be one letter after sentenceBeforeWord
, I.E the startIndex of your word should be equal to sentenceBeforeWord
's length, I think). And you already know the length of your word.
I'm not sure how to create NSRange
(NSString)- or Range
(String)-objects from the top of my head, but you might be able to figure it out.
You might also have to fix some of the logic in my loop, as I don't know what words[0..<index]
will do in the first loop (0..<0).
Sidenote, be careful when you create NSRange
- and/or Range
-objects. One letter is not always one letter. If I recall correctly, it's not always correct to assume that the length of a string's range is equal to the number of characters, as some characters, such as emojis, have a different range- and character-system than regular alphanumerical letters. One emoji might be one letter, but I do believe it has the range of two.
回答3:
Your code seems to work fine, are you able to provide an example where it doesn't work?
There are a couple of small improvements you can make however:
let text: NSString = "#hellothere I am you. #hashtag #hashtag2 @you #hashtag3"
let words = text.components(separatedBy: " ").filter { $0.hasPrefix("#") }
let attributed = NSMutableAttributedString(string: text as String)
for word in words {
attributed.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: text.range(of: word))
}
- There is no need to call
NSMutableAttributedString.init(string: text)
specifically, you can useNSMutableAttributedString(string: text)
- You can use filter on the array to reduce it down to the hashtag values straight away dismissing the need to check for a hashtag during your for loop.
- I would suggest declaring the string as an NSString so that you don't need to cast it throughout each iteration - you will need to cast it for the NSMutableAttributedString initialisation however.
来源:https://stackoverflow.com/questions/42872815/color-hashtags-in-nsattributedstring