This has been bugging me for a long time. As the title says, I do not see any other way to come with an idea to highlight some particular words with various colors. For example, if I have an array that contains words, "hello", "awesome", "hungry", "sky"...and so on. Then in the code below, it just highlights these words inside the array with one color, which is set by NSBackgroundColorAttributeName. So my best solution so far, which failed, was to create multiple of the same method. So I made several highlightColors methods and entered an array in each method and changed different color for each.
A word entered on my TextView became blue. After another word from another array was entered on my TextView, the newly entered word became red but the previous word were not highlighted anymore. I want all words from different arrays to remain highlighted differently. I never want their colors to disappear. I just cannot come up with this idea for a week. Could you please help me out with this?
func highlightColors(type: [String]){
let attrStr = NSMutableAttributedString(string: myTextView.text)
let inputLength = attrStr.string.characters.count
let searchString : NSArray = NSArray.init(array: type)
for i in 0...searchString.count-1 {
let string : String = searchString.object(at: i) as! String
let searchLength = string.characters.count
var range = NSRange(location: 0, length: attrStr.length)
while (range.location != NSNotFound) {
range = (attrStr.string as NSString).range(of: string, options: [], range: range)
if (range.location != NSNotFound) {
coloredStringArray.append(string)
attrStr.addAttribute(NSBackgroundColorAttributeName, value: UIColor.blue, range: NSRange(location: range.location, length: searchLength))
attrStr.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 20), range: range)
range = NSRange(location: range.location + range.length, length: inputLength - (range.location + range.length))
myTextView.attributedText = attrStr
myTextView.font = UIFont(name: "times new roman", size: 20.0)
}
}
}
}
Here is an example of using NSAttributedString to highlight certain words with certain colours. You can modify it any way you want to highlight with certain fonts and backgrounds, etc.
The idea is that you search the text view's text for a string then modify its attributed string with the required attributes.
The reason your attributes are probably disappearing when you edit the textView is because you are using textView.text = blah..
private var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
self.textView = UITextView()
self.view.addSubview(self.textView)
self.textView.leftAnchor.constraint(equalTo: self.view.leftAnchor).isActive = true
self.textView.rightAnchor.constraint(equalTo: self.view.rightAnchor).isActive = true
self.textView.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
self.textView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true
self.textView.translatesAutoresizingMaskIntoConstraints = false
self.textView.text = "Test Highlighting Colors on StackOverflow."
self.highlight(text: ["Test", "Highlighting", "Colors", "StackOverflow", "Test"], colours: [UIColor.red, UIColor.blue, UIColor.green, UIColor.purple, UIColor.cyan])
}
func highlight(text: [String], colours: [UIColor]) {
var ranges = Array<Int>()
let attrString = self.textView.attributedText.mutableCopy() as! NSMutableAttributedString
for i in 0..<text.count {
let str = text[i]
let col = colours[i]
var range = (self.textView.text as NSString).range(of: str)
while (range.location != NSNotFound && ranges.index(of: range.location) != nil) {
let subRange = NSMakeRange(range.location + 1, self.textView.text.characters.count - range.location - 1)
range = (self.textView.text as NSString).range(of: str, options: NSString.CompareOptions.literal, range: subRange)
}
if range.location != NSNotFound {
ranges.append(range.location)
attrString.addAttribute(NSForegroundColorAttributeName, value: col, range: range)
}
}
self.textView.attributedText = attrString
}
I want to get ride of the white spaces in front and at the end of my NSAttributedString(Trimming it). I can't simply convert it to string and do trimming because there are images(attachments) in it.
How can i do it?
Create extension of NSAttributedString as below.
extension NSAttributedString {
public func attributedStringByTrimmingCharacterSet(charSet: CharacterSet) -> NSAttributedString {
let modifiedString = NSMutableAttributedString(attributedString: self)
modifiedString.trimCharactersInSet(charSet: charSet)
return NSAttributedString(attributedString: modifiedString)
}
}
extension NSMutableAttributedString {
public func trimCharactersInSet(charSet: CharacterSet) {
var range = (string as NSString).rangeOfCharacter(from: charSet as CharacterSet)
// Trim leading characters from character set.
while range.length != 0 && range.location == 0 {
replaceCharacters(in: range, with: "")
range = (string as NSString).rangeOfCharacter(from: charSet)
}
// Trim trailing characters from character set.
range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
while range.length != 0 && NSMaxRange(range) == length {
replaceCharacters(in: range, with: "")
range = (string as NSString).rangeOfCharacter(from: charSet, options: .backwards)
}
}
}
and use in viewController where you want to use. like this
let attstring = NSAttributedString(string: "this is test message. Please wait. ")
let result = attstring.attributedStringByTrimmingCharacterSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
This works even with emoji in the text
extension NSAttributedString {
/** Will Trim space and new line from start and end of the text */
public func trimWhiteSpace() -> NSAttributedString {
let invertedSet = CharacterSet.whitespacesAndNewlines.inverted
let startRange = string.utf16.description.rangeOfCharacter(from: invertedSet)
let endRange = string.utf16.description.rangeOfCharacter(from: invertedSet, options: .backwards)
guard let startLocation = startRange?.upperBound, let endLocation = endRange?.lowerBound else {
return NSAttributedString(string: string)
}
let location = string.utf16.distance(from: string.startIndex, to: startLocation) - 1
let length = string.utf16.distance(from: startLocation, to: endLocation) + 2
let range = NSRange(location: location, length: length)
return attributedSubstring(from: range)
}
}
USAGE
let attributeString = NSAttributedString(string: "\n\n\n Hi π π©βπ©βπ§π©βπ©βπ¦βπ¦π©βπ©βπ§βπ§π¨βπ¨βπ¦π©βπ¦π¨βπ¨βπ§βπ§π¨βπ¨βπ¦βπ¦π¨βπ¨βπ§βπ¦π©βπ§βπ¦π©βπ¦βπ¦π©βπ§βπ§π¨βπ¦ buddy. ")
let result = attributeString.trimWhiteSpace().string // Hi π π©βπ©βπ§π©βπ©βπ¦βπ¦π©βπ©βπ§βπ§π¨βπ¨βπ¦π©βπ¦π¨βπ¨βπ§βπ§π¨βπ¨βπ¦βπ¦π¨βπ¨βπ§βπ¦π©βπ§βπ¦π©βπ¦βπ¦π©βπ§βπ§π¨βπ¦ buddy.
Swift 4 and above
extension NSMutableAttributedString {
func trimmedAttributedString() -> NSAttributedString {
let invertedSet = CharacterSet.whitespacesAndNewlines.inverted
let startRange = string.rangeOfCharacter(from: invertedSet)
let endRange = string.rangeOfCharacter(from: invertedSet, options: .backwards)
guard let startLocation = startRange?.upperBound, let endLocation = endRange?.lowerBound else {
return NSAttributedString(string: string)
}
let location = string.distance(from: string.startIndex, to: startLocation) - 1
let length = string.distance(from: startLocation, to: endLocation) + 2
let range = NSRange(location: location, length: length)
return attributedSubstring(from: range)
}
}
use:
let string = "This is string with some space in the end. "
let attributedText = NSMutableAttributedString(string: string).trimmedAttributedString()
It turns out that Unicode strings are hard hahaha! The other solutions posted here are a great starting point, but they crashed for me when using non-latin strings.
Whenever using indexes or ranges in Swift Strings, we need to use String.Index instead of plain Int. Creating an NSRange from a Range<String.Index> has to be done with NSRange(swiftRange, in: String).
That being said, this code builds on the other answers, but makes it unicode-proof:
public extension NSMutableAttributedString {
/// Trims new lines and whitespaces off the beginning and the end of attributed strings
func trimmedAttributedString() -> NSAttributedString {
let invertedSet = CharacterSet.whitespacesAndNewlines.inverted
let startRange = string.rangeOfCharacter(from: invertedSet)
let endRange = string.rangeOfCharacter(from: invertedSet, options: .backwards)
guard let startLocation = startRange?.lowerBound, let endLocation = endRange?.lowerBound else {
return NSAttributedString(string: string)
}
let trimmedRange = startLocation...endLocation
return attributedSubstring(from: NSRange(trimmedRange, in: string))
}
}
I made a swift 3 implementation, just in case anyone is interested:
/**
Trim an attributed string. Can for example be used to remove all leading and trailing spaces and line breaks.
*/
public func attributedStringByTrimmingCharactersInSet(set: CharacterSet) -> NSAttributedString {
let invertedSet = set.inverted
let rangeFromStart = string.rangeOfCharacter(from: invertedSet)
let rangeFromEnd = string.rangeOfCharacter(from: invertedSet, options: .backwards)
if let startLocation = rangeFromStart?.upperBound, let endLocation = rangeFromEnd?.lowerBound {
let location = string.distance(from: string.startIndex, to: startLocation) - 1
let length = string.distance(from: startLocation, to: endLocation) + 2
let newRange = NSRange(location: location, length: length)
return self.attributedSubstring(from: newRange)
} else {
return NSAttributedString()
}
}
Swift 3.2 Version:
extension NSAttributedString {
public func trimmingCharacters(in characterSet: CharacterSet) -> NSAttributedString {
let modifiedString = NSMutableAttributedString(attributedString: self)
modifiedString.trimCharacters(in: characterSet)
return NSAttributedString(attributedString: modifiedString)
}
}
extension NSMutableAttributedString {
public func trimCharacters(in characterSet: CharacterSet) {
var range = (string as NSString).rangeOfCharacter(from: characterSet)
// Trim leading characters from character set.
while range.length != 0 && range.location == 0 {
replaceCharacters(in: range, with: "")
range = (string as NSString).rangeOfCharacter(from: characterSet)
}
// Trim trailing characters from character set.
range = (string as NSString).rangeOfCharacter(from: characterSet, options: .backwards)
while range.length != 0 && NSMaxRange(range) == length {
replaceCharacters(in: range, with: "")
range = (string as NSString).rangeOfCharacter(from: characterSet, options: .backwards)
}
}
}
Following code will work for your requirement.
var attString: NSAttributedString = NSAttributedString(string: " this is att string")
let trimmedString = attString.string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceCharacterSet())
I'm trying to color all occurrences (not just the first) of a textview string however when I get to the looping I get a cannot invoke rangeOfString argument.
I checked the rangeOfString:options:range documentation on Swift 2 and it looked pretty similar. I'm not really sure what I'm doing wrong.
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var textView: UITextView!
func textView(textView: UITextView){
if textView == self.textView {
let nsString = textView.text as NSString
let stringLength = textView.text.characters.count
var range = NSRange(location: 0, length: textView.text.characters.count)
let searchString = textView.text
let text = NSMutableAttributedString(string: textView.text)
text.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, stringLength))
textView.attributedText = text
while(range.location != NSNotFound) {
range = (textView.text as NSString).rangeOfString(searchString, options: NSStringCompareOptions, range: range)
text.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: nsString.rangeOfString("hello"))
}
}
}
}
It looks like you try to highlight user input from search bar. Here how I do so:
func highlightedText(text: NSString, inText: NSString, var withColor: UIColor?) -> NSMutableAttributedString {
var attributedString = NSMutableAttributedString(string:inText as String)
let range = (inText.lowercaseString as NSString).rangeOfString(text.lowercaseString as String)
if withColor == nil {
if let color = globalTint() {
withColor = color
} else {
withColor = UIColor.redColor()
}
}
attributedString.addAttribute(NSForegroundColorAttributeName, value: withColor! , range: range)
return attributedString
}
...
textView.attributedText = highlightedText("string", inText: titleText, color: nil)
Problem: NSAttributedString takes an NSRange while I'm using a Swift String that uses Range
let text = "Long paragraph saying something goes here!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)
text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
Produces the following error:
error: 'Range' is not convertible to 'NSRange'
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
Swift String ranges and NSString ranges are not "compatible".
For example, an emoji like π counts as one Swift character, but as two NSString
characters (a so-called UTF-16 surrogate pair).
Therefore your suggested solution will produce unexpected results if the string
contains such characters. Example:
let text = "πππLong paragraph saying!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)
text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
let start = distance(text.startIndex, substringRange.startIndex)
let length = distance(substringRange.startIndex, substringRange.endIndex)
let range = NSMakeRange(start, length)
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
}
})
println(attributedString)
Output:
πππLong paragra{
}ph say{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}ing!{
}
As you see, "ph say" has been marked with the attribute, not "saying".
Since NS(Mutable)AttributedString ultimately requires an NSString and an NSRange, it is actually
better to convert the given string to NSString first. Then the substringRange
is an NSRange and you don't have to convert the ranges anymore:
let text = "πππLong paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: nsText)
nsText.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
println(attributedString)
Output:
πππLong paragraph {
}saying{
NSColor = "NSCalibratedRGBColorSpace 1 0 0 1";
}!{
}
Update for Swift 2:
let text = "πππLong paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)
nsText.enumerateSubstringsInRange(textRange, options: .ByWords, usingBlock: {
(substring, substringRange, _, _) in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: substringRange)
}
})
print(attributedString)
Update for Swift 3:
let text = "πππLong paragraph saying!"
let nsText = text as NSString
let textRange = NSMakeRange(0, nsText.length)
let attributedString = NSMutableAttributedString(string: text)
nsText.enumerateSubstrings(in: textRange, options: .byWords, using: {
(substring, substringRange, _, _) in
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.red, range: substringRange)
}
})
print(attributedString)
Update for Swift 4:
As of Swift 4 (Xcode 9), the Swift standard library
provides method to convert between Range<String.Index> and NSRange.
Converting to NSString is no longer necessary:
let text = "πππLong paragraph saying!"
let attributedString = NSMutableAttributedString(string: text)
text.enumerateSubstrings(in: text.startIndex..<text.endIndex, options: .byWords) {
(substring, substringRange, _, _) in
if substring == "saying" {
attributedString.addAttribute(.foregroundColor, value: NSColor.red,
range: NSRange(substringRange, in: text))
}
}
print(attributedString)
Here substringRange is a Range<String.Index>, and that is converted to the
corresponding NSRange with
NSRange(substringRange, in: text)
For cases like the one you described, I found this to work. It's relatively short and sweet:
let attributedString = NSMutableAttributedString(string: "follow the yellow brick road") //can essentially come from a textField.text as well (will need to unwrap though)
let text = "follow the yellow brick road"
let str = NSString(string: text)
let theRange = str.rangeOfString("yellow")
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.yellowColor(), range: theRange)
The answers are fine, but with Swift 4 you could simplify your code a bit:
let text = "Test string"
let substring = "string"
let substringRange = text.range(of: substring)!
let nsRange = NSRange(substringRange, in: text)
Be cautious, as the result of range function has to be unwrapped.
Possible Solution
Swift provides distance() which measures the distance between start and end that can be used to create an NSRange:
let text = "Long paragraph saying something goes here!"
let textRange = text.startIndex..<text.endIndex
let attributedString = NSMutableAttributedString(string: text)
text.enumerateSubstringsInRange(textRange, options: NSStringEnumerationOptions.ByWords, { (substring, substringRange, enclosingRange, stop) -> () in
let start = distance(text.startIndex, substringRange.startIndex)
let length = distance(substringRange.startIndex, substringRange.endIndex)
let range = NSMakeRange(start, length)
// println("word: \(substring) - \(d1) to \(d2)")
if (substring == "saying") {
attributedString.addAttribute(NSForegroundColorAttributeName, value: NSColor.redColor(), range: range)
}
})
For me this works perfectly:
let font = UIFont.systemFont(ofSize: 12, weight: .medium)
let text = "text"
let attString = NSMutableAttributedString(string: "exemple text :)")
attString.addAttributes([.font: font], range:(attString.string as NSString).range(of: text))
label.attributedText = attString
Swift 4:
Sure, I know that Swift 4 has an extension for NSRange already
public init<R, S>(_ region: R, in target: S) where R : RangeExpression,
S : StringProtocol,
R.Bound == String.Index, S.Index == String.Index
I know in most cases this init is enough. See its usage:
let string = "Many animals here: πΆπ¦π± !!!"
if let range = string.range(of: "πΆπ¦π±"){
print((string as NSString).substring(with: NSRange(range, in: string))) // "πΆπ¦π±"
}
But conversion can be done directly from Range< String.Index > to NSRange without Swift's String instance.
Instead of generic init usage which requires from you the target parameter as String and
if you don't have target string at hand you can create conversion directly
extension NSRange {
public init(_ range:Range<String.Index>) {
self.init(location: range.lowerBound.encodedOffset,
length: range.upperBound.encodedOffset -
range.lowerBound.encodedOffset) }
}
or you can create the specialized extension for Range itself
extension Range where Bound == String.Index {
var nsRange:NSRange {
return NSRange(location: self.lowerBound.encodedOffset,
length: self.upperBound.encodedOffset -
self.lowerBound.encodedOffset)
}
}
Usage:
let string = "Many animals here: πΆπ¦π± !!!"
if let range = string.range(of: "πΆπ¦π±"){
print((string as NSString).substring(with: NSRange(range))) // "πΆπ¦π±"
}
or
if let nsrange = string.range(of: "πΆπ¦π±")?.nsRange{
print((string as NSString).substring(with: nsrange)) // "πΆπ¦π±"
}
Swift 5:
Due to the migration of Swift strings to UTF-8 encoding by default, the usage of encodedOffset is considered as deprecated and Range cannot be converted to NSRange without an instance of String itself, because in order to calculate the offset we need the source string which is encoded in UTF-8 and it should be converted to UTF-16 before calculating offset. So best approach, for now, is to use generic init.
Swift 5 Solution
Converting Range into NSRange
As the 'encodedOffset' is deprecated, so now in order to convert String.Index to Int we need the reference of original string from which Range<String.Index> was derived.
A convenient detailed extension for NSRange could be as below:
extension NSRange {
public init(range: Range<String.Index>,
originalText: String) {
let range_LowerBound_INDEX = range.lowerBound
let range_UpperBound_INDEX = range.upperBound
let range_LowerBound_INT = range_LowerBound_INDEX.utf16Offset(in: originalText)
let range_UpperBound_INT = range_UpperBound_INDEX.utf16Offset(in: originalText)
let locationTemp = range_LowerBound_INT
let lengthTemp = range_UpperBound_INT - range_LowerBound_INT
self.init(location: locationTemp,
length: lengthTemp)
}
}
While the shorthand extension is as below
extension NSRange {
public init(range: Range<String.Index>,
originalText: String) {
self.init(location: range.lowerBound.utf16Offset(in: originalText),
length: range.upperBound.utf16Offset(in: originalText) - range.lowerBound.utf16Offset(in: originalText))
}
}
Now we can use any Range to convert it into NSRange as below, sharing my own requirement which led me to write above extensions
I was using below String extension for finding all the ranges of specific word from the String
extension String {
func ranges(of substring: String, options: CompareOptions = [], locale: Locale? = nil) -> [Range<Index>] {
var ranges: [Range<Index>] = []
while let range = range(of: substring, options: options, range: (ranges.last?.upperBound ?? self.startIndex)..<self.endIndex, locale: locale) {
ranges.append(range)
}
return ranges
}
}
My requirement was to change the colour of specific words in a String, so for that I wrote this extension which does the job
extension NSAttributedString {
static func colored(originalText:String,
wordToColor:String,
currentColor:UIColor,
differentColor:UIColor) -> NSAttributedString {
let attr = NSMutableAttributedString(string: originalText)
attr.beginEditing()
attr.addAttribute(NSAttributedString.Key.foregroundColor,
value: currentColor,
range: NSRange(location: 0, length: originalText.count))
// FOR COVERING ALL THE OCCURENCES
for eachRange in originalText.ranges(of: wordToColor) {
attr.addAttribute(NSAttributedString.Key.foregroundColor,
value: differentColor,
range: NSRange(range: eachRange, originalText: originalText))
}
attr.endEditing()
return attr
}
}
Finally I was using it from my main code as below
let text = "Collected".localized() + " + " + "Cancelled".localized() + " + " + "Pending".localized()
myLabel.attributedText = NSAttributedString.colored(originalText: text,
wordToColor: "+",
currentColor: UIColor.purple,
differentColor: UIColor.blue)
And the result is as below, having the colour of + sign changed as blue from the main text colour which is purple.
Hope this helps someone in need. Thanks!
Swift 4
I think, there are two ways.
1. NSRange(range, in: )
2. NSRange(location:, length: )
Sample code:
let attributedString = NSMutableAttributedString(string: "Sample Text 12345", attributes: [.font : UIFont.systemFont(ofSize: 15.0)])
// NSRange(range, in: )
if let range = attributedString.string.range(of: "Sample") {
attributedString.addAttribute(.foregroundColor, value: UIColor.orange, range: NSRange(range, in: attributedString.string))
}
// NSRange(location: , length: )
if let range = attributedString.string.range(of: "12345") {
attributedString.addAttribute(.foregroundColor, value: UIColor.green, range: NSRange(location: range.lowerBound.encodedOffset, length: range.upperBound.encodedOffset - range.lowerBound.encodedOffset))
}
Screen Shot:
Swift 3 Extension Variant that preserves existing attributes.
extension UILabel {
func setLineHeight(lineHeight: CGFloat) {
guard self.text != nil && self.attributedText != nil else { return }
var attributedString = NSMutableAttributedString()
if let attributedText = self.attributedText {
attributedString = NSMutableAttributedString(attributedString: attributedText)
} else if let text = self.text {
attributedString = NSMutableAttributedString(string: text)
}
let style = NSMutableParagraphStyle()
style.lineSpacing = lineHeight
style.alignment = self.textAlignment
let str = NSString(string: attributedString.string)
attributedString.addAttribute(NSParagraphStyleAttributeName,
value: style,
range: str.range(of: str as String))
self.attributedText = attributedString
}
}
func formatAttributedStringWithHighlights(text: String, highlightedSubString: String?, formattingAttributes: [String: AnyObject]) -> NSAttributedString {
let mutableString = NSMutableAttributedString(string: text)
let text = text as NSString // convert to NSString be we need NSRange
if let highlightedSubString = highlightedSubString {
let highlightedSubStringRange = text.rangeOfString(highlightedSubString) // find first occurence
if highlightedSubStringRange.length > 0 { // check for not found
mutableString.setAttributes(formattingAttributes, range: highlightedSubStringRange)
}
}
return mutableString
}
I love the Swift language, but using NSAttributedString with a Swift Range that is not compatible with NSRange has made my head hurt for too long. So to get around all that garbage I devised the following methods to return an NSMutableAttributedString with the highlighted words set with your color.
This does not work for emojis. Modify if you must.
extension String {
func getRanges(of string: String) -> [NSRange] {
var ranges:[NSRange] = []
if contains(string) {
let words = self.components(separatedBy: " ")
var position:Int = 0
for word in words {
if word.lowercased() == string.lowercased() {
let startIndex = position
let endIndex = word.characters.count
let range = NSMakeRange(startIndex, endIndex)
ranges.append(range)
}
position += (word.characters.count + 1) // +1 for space
}
}
return ranges
}
func highlight(_ words: [String], this color: UIColor) -> NSMutableAttributedString {
let attributedString = NSMutableAttributedString(string: self)
for word in words {
let ranges = getRanges(of: word)
for range in ranges {
attributedString.addAttributes([NSForegroundColorAttributeName: color], range: range)
}
}
return attributedString
}
}
Usage:
// The strings you're interested in
let string = "The dog ran after the cat"
let words = ["the", "ran"]
// Highlight words and get back attributed string
let attributedString = string.highlight(words, this: .yellow)
// Set attributed string
label.attributedText = attributedString
My solution is a string extension that first gets the swift range then get's the distance from the start of the string to the start and end of the substring.
These values are then used to calculate the start and length of the substring. We can then apply these values to the NSMakeRange constructor.
This solution works with substrings that consist of multiple words, which a lot of the solutions here using enumerateSubstrings let me down on.
extension String {
func NSRange(of substring: String) -> NSRange? {
// Get the swift range
guard let range = range(of: substring) else { return nil }
// Get the distance to the start of the substring
let start = distance(from: startIndex, to: range.lowerBound) as Int
//Get the distance to the end of the substring
let end = distance(from: startIndex, to: range.upperBound) as Int
//length = endOfSubstring - startOfSubstring
//start = startOfSubstring
return NSMakeRange(start, end - start)
}
}
let text:String = "Hello Friend"
let searchRange:NSRange = NSRange(location:0,length: text.characters.count)
let range:Range`<Int`> = Range`<Int`>.init(start: searchRange.location, end: searchRange.length)