Casting a Range<String.Index> to NSRange [duplicate] - ios

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)

Related

How to set custom font with regular and bold font while setting html string to label in swift 4?

I am getting HTML formatted string from API response, so I need to set it to a label while maintaining Custom Font(as of my App) and also applying a style(bold, regular, etc.) to the label.
I have used an extension that enables to convert the HTML string to regular string with newlines etc. but, I was able to set font here, but only one font and it shows in regular font only, so the whole label is in one font, what I want is to set bold font to the bold HTML part and regular to regular HTML part/tag.
extension String {
var htmlToAttributedString: NSAttributedString {
guard let data = data(using: .utf8) else { return NSAttributedString() }
do {
return try NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding:String.Encoding.utf8.rawValue], documentAttributes: nil)
} catch {
return NSAttributedString()
}
}
var htmlToString: String {
return htmlToAttributedString.string
}
}
//set converted html to string here
let whyAttendAttributedText: NSMutableAttributedString = NSMutableAttributedString(attributedString: attendData.whyAttendData?.desc?.htmlToAttributedString ?? NSAttributedString())
//set font here
whyAttendAttributedText.addAttributes([NSMutableAttributedString.Key.font: CommonSettings.shared.getFont(type: .regular, size: descriptionLabel.font.pointSize), NSMutableAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(0, whyAttendAttributedText.length))
I want to set bold and regular to the text, but as I have set only one font I was unable to get the result, is there any way to set the bold and regular font as in HTML string?
This should help :
extension String {
func attributedString(withRegularFont regularFont: UIFont, andBoldFont boldFont: UIFont) -> NSMutableAttributedString {
var attributedString = NSMutableAttributedString()
guard let data = self.data(using: .utf8) else { return NSMutableAttributedString() }
do {
attributedString = try NSMutableAttributedString(data: data,
options: [.documentType: NSAttributedString.DocumentType.html,
.characterEncoding:String.Encoding.utf8.rawValue],
documentAttributes: nil)
let range = NSRange(location: 0, length: attributedString.length)
attributedString.enumerateAttribute(NSAttributedString.Key.font, in: range, options: .longestEffectiveRangeNotRequired) { value, range, _ in
let currentFont: UIFont = value as! UIFont
var replacementFont: UIFont? = nil
if currentFont.fontName.contains("bold") || currentFont.fontName.contains("Bold") {
replacementFont = boldFont
} else {
replacementFont = regularFont
}
let replacementAttribute = [NSAttributedString.Key.font:replacementFont!]
attributedString.addAttributes(replacementAttribute, range: range)
}
} catch let e {
print(e.localizedDescription)
}
return attributedString
}
}
Let's assume your string after parsing HTML string is: "This is your HTML string"
To create an attributed string,
let attrStr = NSMutableAttributedString(string: "This is your HTML string")
Adding UIFont attribute with value as System-Regular,
attrStr.addAttribute(.font, value: UIFont.systemFont(ofSize: 14.0, weight: .regular), range: NSRange(location: 0, length: attrStr.length))
Whenever adding an attribute to the attributed string, we need to provide the range of string in which we want to reflect the attribute.
Since we need the whole string to have Regular font, so the range is calculated as the whole string length.
Now, adding UIFont attribute with value as System-Bold to a part of the string, let's say we make HTML as bold,
attrStr.addAttribute(.font, value: UIFont.systemFont(ofSize: 14.0, weight: .bold), range: (attrStr.string as NSString).range(of: "HTML"))
We've calculated the range of HTML word within the whole string.
Similarly, you can add any of the attributes to the string giving the relevant range values.
Output: This is yourHTMLstring
Edit-1:
To calculate the range of <b> to </b> you need to calculate it manually.
Example:
let str = "This <b>is your HTML</b> string"
let range1 = (str as NSString).range(of: "<b>")
let range2 = (str as NSString).range(of: "</b>")
let requiredRange = NSRange(location: range1.location, length: range2.location + range2.length - range1.location)
The above example will work for single instance of <b>/</b> in the string.
Edit-2:
When string includes multiple instances of <b>/</b>:
let htmlStr = "This is an <b>HTML</b> parsed <b>string</b>"
let arr = htmlStr.components(separatedBy: "</b>")
let attrStr = NSMutableAttributedString()
for str in arr {
if !str.isEmpty {
let range1 = (str as NSString).range(of: "<b>")
let requiredRange = NSRange(location: range1.location, length: str.count - range1.location)
let formattedStr = NSMutableAttributedString(string: str)
formattedStr.addAttribute(.font, value: UIFont.systemFont(ofSize: 14.0, weight: .bold), range: requiredRange)
attrStr.append(formattedStr)
attrStr.append(NSAttributedString.init(string: "</b>", attributes: [.font : UIFont.systemFont(ofSize: 14.0, weight: .bold)]))
}
}
self.label.attributedText = attrStr
Output: This is an<b>HTML</b>parsed<b>string</b>
Applying Bold and other different styles to the text can be done using below method.
extension String {
func attributedString(with style: [NSAttributedString.Key: Any]? = nil,
and highlightedText: String,
with highlightedTextStyle: [NSAttributedString.Key: Any]? = nil) -> NSAttributedString {
let formattedString = NSMutableAttributedString(string: self, attributes: style)
let highlightedTextRange: NSRange = (self as NSString).range(of: highlightedText as String)
formattedString.setAttributes(highlightedTextStyle, range: highlightedTextRange)
return formattedString
}
}
Input: "This is a test message"
Expectd Output: "This is a test message"
This can be achieved as follows.
let sampleInput = "This is a test message"
let boldtext = "test"
let output = sampleInput.attributedString(with: [.font: UIFont.systemFont(ofSize: 12.0, weight: .regular)],
and: boldtext, with: UIFont.systemFont(ofSize: 12.0, weight: .bold))
Different styles can be applied using different attribute keys. Hope this helps.

NSAttributedString not working for UILabel

I looked around SO and couldn't find this exact problem, despite there being a few questions with similar titles.
All I want to do is have some matching text on UILabel be drawn in BOLD. I'm using it when I'm searching for objects, it should 'bolden' the search term. To this aim, I wrote the following code:
extension String {
func boldenOccurrences(of searchTerm: String?, baseFont: UIFont, textColor: UIColor) -> NSAttributedString {
let defaultAttributes: [String : Any] = [NSForegroundColorAttributeName : textColor,
NSFontAttributeName: baseFont]
let result = NSMutableAttributedString(string: self, attributes: defaultAttributes)
guard let searchTerm = searchTerm else {
return result
}
guard searchTerm.characters.count > 0 else {
return result
}
// Ranges. Crash course:
//let testString = "Holy Smokes!"
//let range = testString.startIndex ..< testString.endIndex
//let substring = testString.substring(with: range) // is the same as testString
var searchRange = self.startIndex ..< self.endIndex //whole string
var foundRange: Range<String.Index>!
let boldFont = UIFont(descriptor: baseFont.fontDescriptor.withSymbolicTraits(.traitBold)!, size: baseFont.pointSize)
repeat {
foundRange = self.range(of: searchTerm, options: .caseInsensitive , range: searchRange)
if let found = foundRange {
// now we have to do some stupid stuff to make Range compatible with NSRange
let rangeStartIndex = found.lowerBound
let rangeEndIndex = found.upperBound
let start = self.distance(from: self.startIndex, to: rangeStartIndex)
let length = self.distance(from: rangeStartIndex, to: rangeEndIndex)
log.info("Bolden Text: \(searchTerm) in \(self), range: \(start), \(length)")
let nsRange = NSMakeRange(start, length)
result.setAttributes([NSForegroundColorAttributeName : textColor,
NSFontAttributeName: boldFont], range: nsRange)
searchRange = found.upperBound ..< self.endIndex
}
} while foundRange != nil
return result
}
}
Everything "looks" fine. The log statement spits out what I expect and it's all good. However, when drawn on the UILabel, sometimes an entire string is set to bold, and I don't understand how that could be happening. Nothing in the code suggests this should be happening.
I set the result of this above method in a typical UITableCell configuration method (i.e. tableView(cellForRowAt indexPath:.... ) )
cell.titleLabel.attributedText = artist.displayName.emptyIfNil.boldenOccurrences(of: source.currentSearchTerm, baseFont: cell.titleLabel.font, textColor: cell.titleLabel.textColor)
Your primary issue is the cell reuse, maybe when a cell is reused keep your font bold as font, and that is why you have this issue, you can solve this in your cell prepareForReuse() method you can add
override func prepareForReuse() {
super.prepareForReuse()
//to fix ipad Error
self.titleLabel.font = UIFont(name: "YourBaseFont", size: yourFontSize)
}

How to replace a html tag with nsattribute in swift?

For eg. replace a <strong> tag with bold font. So that it can be applied as attributed text to label, textfield or textview
extension NSAttributedString {
func replaceHTMLTag(tag: String, withAttributes attributes: [String: AnyObject]) -> NSAttributedString {
let openTag = "<\(tag)>"
let closeTag = "</\(tag)>"
let resultingText: NSMutableAttributedString = self.mutableCopy() as! NSMutableAttributedString
while true {
let plainString = resultingText.string as NSString
let openTagRange = plainString.range(of: openTag)
if openTagRange.length == 0 {
break
}
let affectedLocation = openTagRange.location + openTagRange.length
let searchRange = NSRange(location: affectedLocation, length: plainString.length - affectedLocation)
let closeTagRange = plainString.range(of: closeTag, options: NSString.CompareOptions.init(rawValue: 0), range: searchRange)
resultingText.setAttributes(attributes, range: NSRange(location: affectedLocation, length: closeTagRange.location - affectedLocation))
resultingText.deleteCharacters(in: closeTagRange)
resultingText.deleteCharacters(in: openTagRange)
}
return resultingText as NSAttributedString
}
}
You need to remove the ^ and $ anchors.
The ^ means start of string and $ means end of string (or line, depending on the options). That's why your first example works: in the first test string, the start of the string is really followed by your pattern and ends with it.
In the second test string, the pattern is found in the middle of the string, thus the ^... can't apply. If you would just remove the ^, the $ would apply on the second occurrence of the registration number and the output would be my car reg 1 - DD11 AAA my car reg 2 - XX.
let myString = "my car reg 1 - DD11 AAA my car reg 2 - AA22 BBB"
let regex = try! NSRegularExpression(pattern: "([A-HK-PRSVWY][A-HJ-PR-Y])\\s?([0][2-9]|[1-9][0-9])\\s?[A-HJ-PR-Z]{3}", options: NSRegularExpressionOptions.CaseInsensitive)
let range = NSMakeRange(0, myString.characters.count)
let modString = regex.stringByReplacingMatchesInString(myString, options: [], range: range, withTemplate: "XX")
print(modString)
// Output: "my car reg 1 - XX my car reg 2 - XX"
Reference: https://stackoverflow.com/a/28503676/8169585
Try this extension. It will convert HTML string into NSAttributtedString. I also has the 0.75 ratio applied because of the fact that the default conversion increases all the font size
extension String {
func htmlAttributedString() -> NSAttributedString? {
guard let data = self.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
guard let html = try? NSMutableAttributedString(
data: data,
options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
documentAttributes: nil) else { return nil }
html.beginEditing()
html.enumerateAttribute(NSFontAttributeName, in: NSMakeRange(0, html.length), options: .init(rawValue: 0)) {
(value, range, stop) in
if let font = value as? UIFont {
let resizedFont = font.withSize(font.pointSize * 0.75)
html.addAttribute(NSFontAttributeName,
value: resizedFont,
range: range)
}
}
html.endEditing()
return html
}
}

How can I italicise a certain word (which occurs multiple times) in my string in Swift?

I have a text that reads "Hello world. The world is so beautiful." I want to change it to "Hello world. The world is so beautiful."
I tried the following code but it only changes the first word.
let string : NSString = "Hello world. The world is so beautiful."
let attributedString = NSMutableAttributedString(string: string! as String, attributes: [NSFontAttributeName:UIFont.systemFont(ofSize: 17.0)])
let italicFontAttribute = [NSFontAttributeName: UIFont.italicSystemFont(ofSize: 17.0)]
attributedString.addAttributes(italicFontAttribute, range: (string!.range(of: "world")))
Another solution added to the others, but which makes a search for the specific term and replaces all of the occurrences, could help you creating a general method for this:
var sampleString : String = "Hello world. The world is so beautiful."
let searchTerm : String = "world"
var searchRange : NSRange = NSMakeRange(0, sampleString.characters.count)
var positionRange : NSRange = ((sampleString as NSString).substring(with: searchRange) as NSString).range(of: searchTerm)
var actualRange : NSRange = positionRange
var attributedString : NSMutableAttributedString = NSMutableAttributedString(string: sampleString)
while (positionRange.location != NSNotFound)
{
// Update attributed string
actualRange.location = searchRange.location + positionRange.location
attributedString.addAttributes([ NSFontAttributeName : UIFont(name: "Helvetica-Italic", size: CGFloat(22)) ], range: actualRange)
// Proceed to the next position
searchRange.location += positionRange.location + searchTerm.characters.count
searchRange.length = sampleString.characters.count - searchRange.location
positionRange = ((sampleString as NSString).substring(with: searchRange) as NSString).range(of: searchTerm)
}
attributedString.addAttributes(italicFontAttribute, range: (string!.range(of: "world")))
attributedString.addAttributes(italicFontAttribute, range: (string!.range(of: "World")))
try this capital and small Charter both find in string.

Swift NSAttributedString Trim

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())

Resources