NSRange from Swift Range?
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 stringcontains such characters. Example:
let text = "πππLong paragraph saying!"let textRange = text.startIndex..<text.endIndexlet 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 actuallybetter 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 NSStringlet 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 NSStringlet 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 NSStringlet 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 libraryprovides 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 thecorresponding 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.