Find and replace characters in attributed string with attributes? - swift

I am facing an issue with replacing some characters in an attributed string with some attributed characters.
let replaceString = NSMutableAttributedString(string: playerNames[index], attributes: attributes)
taskText.mutableString.replaceOccurrences(of: searchString[Int(index)], with: playerNames[index], options: .caseInsensitive, range: NSRange(location: 0, length: taskText.length))
The problem is, that I can't add the replaceString to the replaceOccurrences function because I can't use attributed strings here. Is there any method other method?
I just want to replace some "#1" and "#2" strings with some names like "Anna" but with bold font to make it more visible.

Related

Make Ints within a string bold - swift

Hi I have a string that I'm displaying in a label that shows a time like this "4hours 27mins 45secs" and this can vary based on the time for example "30mins 5secs"
I want to style the Ints in the string so they are bold and the characters are light so it looks like this...
I've done some searching and found I need to use some sort of attributed string, but everything I've found seems to use some sort of breaking character, or a standard prefix to detect.
As I don't have either is there a way to check if the character is an int and apply bold. Appreciate this might not be possible and would welcome a recommendation for changing my approach.
Still pretty new to coding/swift so any help appreciated!
Quickly done, you can use a Regular Expression to find the ranges of all the numbers. Then, on these ranges, you change the attribute for a bold font.
let attributedString = NSMutableAttributedString(string: str, attributes: [.foregroundColor: UIColor.white])
let regex = try! NSRegularExpression(pattern: "\\d+", options: [])
let matches = regex.matches(in: attributedString.string, options: [], range: NSRange(location: 0, length: attributedString.length))
matches.forEach {
attributedString.addAttributes([.font: UIFont.boldSystemFont(ofSize: 17)], range: $0.range)
}
If your deployment target is iOS15 you can use the newly added AttributedString with markdown support, which is much simple than the very complicated to use NSAttributedString.
https://developer.apple.com/documentation/foundation/attributedstring

Swift finding and changing range in attributed string

What would be the best way to change this string:
"gain quickness for 5 seconds. <c=#reminder>(Cooldown: 90s)</c> only after"
into an Attributed String while getting rid of the part in the <> and I want to change the font of (Cooldown: 90s). I know how to change and make NSMutableAttributedStrings but I am stuck on how to locate and change just the (Cooldown: 90s) in this case. The text in between the <c=#reminder> & </c> will change so I need to use those to find what I need.
These seem to be indicators meant to be used for this purpose I just don't know ho.
First things first, you'll need a regular expression to find and replace all tagged strings.
Looking at the string, one possible regex could be <c=#([a-zA-Z-9]+)>([^<]*)</c>. Note that will will work only if the string between the tags doesn't contain the < character.
Now that we have the regex, we only need to apply it on the input string:
let str = "gain quickness for 5 seconds. <c=#reminder>(Cooldown: 90s)</c> only after"
let attrStr = NSMutableAttributedString(string: str)
let regex = try! NSRegularExpression(pattern: "<c=#([a-zA-Z-9]+)>([^<]*)</c>", options: [])
while let match = regex.matches(in: attrStr.string, options: [], range: NSRange(location: 0, length: attrStr.string.utf16.count)).first {
let indicator = str[Range(match.range(at: 1), in: str)!]
let substr = str[Range(match.range(at: 2), in: str)!]
let replacement = NSMutableAttributedString(string: String(substr))
// now based on the indicator variable you might want to apply some transformations in the `substr` attributed string
attrStr.replaceCharacters(in: match.range, with: replacement)
}

Adding text to string

I am styling some dynamic markdown, however the framework I am using for styling doesnt support nested tags for links.
I need to parse the string and close the styling markdown tags effectively this :
"__Some bold text [FIRST LINK](https://FIRSTLINK.COM \"FIRST LINK\"), more bold text.__\n\n additional text \n\n
*some italic text[SECOND LINK](https://SECONDLINK.COM) ending text,*"
to this:
"__Some bold text __[FIRST LINK](https://FIRSTLINK.COM \"FIRST LINK\")__, more bold text.__\n\n additional text \n\n
*some italic text*[SECOND LINK](https://SECONDLINK.COM)* ending text,*"
This is only really going to be for bold and italic text. I started going down the route of
var str = "__Some bold text [FIRST LINK](https://FIRSTLINK.COM \"FIRST LINK\"), more bold text.__\n\n additional text \n\n *some italic text[SECOND LINK](https://SECONDLINK.COM) ending text,*"
let bold = str.components(separatedBy: "__")
for var string in bold {
if let matchedIndex = string.index(of: "[") {
string.insert(contentsOf: "__", at: matchedIndex)
}
}
But wondered, is there a better way to do this in Swift?
Edit - for clarity - essentially I need to modify the existing string to have closed tags prior to a link tag and re opened after a link tag - this prevents the links from being nested with the style tags and allows the styler framework to apply attributed strings accordingly
EDIT --- in line with #Linus comment here is the results of the regex (note running these out side of an extension in order to be able to test in a playground
var str = "__Some bold text [FIRST LINK](https://FIRSTLINK.COM \"FIRST LINK\"), more bold text.__\n additional text \n *some italic text[SECOND LINK](https://SECONDLINK.COM) ending text,*\n__sfdadhfjkh [THIRD LINK](https://THIRDLINK.COM \"THIRD LINK\"), more bold text.__"
do {
var regex = try NSRegularExpression(pattern: "(\\[.*?\\))" , options: [.caseInsensitive])
var newString = regex.stringByReplacingMatches(in: str, options: [], range: NSMakeRange(0, str.utf16.count), withTemplate: "__$1__")
print("\nFirst regex __$1__ \n\n\(newString)")
regex = try NSRegularExpression(pattern: "(\\[.*?\\))" , options: [.caseInsensitive])
var newerString = regex.stringByReplacingMatches(in: str, options: [], range: NSMakeRange(0, str.utf16.count), withTemplate: "*$1*")
print("\nSecond Regex *$1* \n\n"+newerString)
} catch { print("ERROR: searchFor regex (\("(\\[.*?\\))")) on string (\(str)) failed") }
Printed results
First regex __$1__
__Some bold text __[FIRST LINK](https://FIRSTLINK.COM "FIRST LINK")__, more bold text.__
additional text
*some italic text__[SECOND LINK](https://SECONDLINK.COM)__ ending text,*
__sfdadhfjkh __[THIRD LINK](https://THIRDLINK.COM "THIRD LINK")__, more bold text.__
Second Regex *$1*
__Some bold text *[FIRST LINK](https://FIRSTLINK.COM "FIRST LINK")*, more bold text.__
additional text
*some italic text*[SECOND LINK](https://SECONDLINK.COM)* ending text,*
__sfdadhfjkh *[THIRD LINK](https://THIRDLINK.COM "THIRD LINK")*, more bold text.__
I need to have both Italic and strong tags amended on the same string in order to pass it to a view to be styled
I'm using the following String extension that allows you to find strings that match a certain regex pattern and replace it with some other string:
extension String {
mutating func replaceOccurrence(ofPattern pattern: String, with replacementString: String) {
do {
let regex = try NSRegularExpression(pattern: pattern, options: [.caseInsensitive])
self = regex.stringByReplacingMatches(in: self, options: [], range: NSMakeRange(0, utf16.count), withTemplate: replacementString)
} catch { print("ERROR: searchFor regex (\(pattern)) on string (\(self)) failed") }
}
}
Then, you could replace (\[.*?\)) with __$1__, like this:
str.replaceOccurrence(ofPattern: "(\\[.*?\\))", with: "__$1__")
Explanation
...in case you're unfamiliar with regular expressions:
The regex:
( - opening parenthesis, that creates a new group which is later used to insert the matched string back into the replacement string
\[ - matches a bracket; needs to be escaped using \ to disable the bracket's regex meaning & match the actual character instead
.* - matches any character...
? - ...until...
\) - ...the next closing parenthesis; this one also needs to be escaped to match the actual character, and not create a new group
) - closes the group
The replacement:
__ - your replacement string: opening bold range in this case
$1 - inserts the previously matched group here
__ - again, your replacement string: closing bold range in this case
Fun-Fact: in Swift, you need to escape escaping characters, like \\ to make sure the code compiles, because Xcode thinks, you're trying to escape a character from the string at compile-time.
That's why the regex isn't (\[.*?\)), but (\\[.*?\\)).

Setting foreground color works only once for NSAttributedString

The phone part of the string gets the underline attribute, but the color remains red. I've separated the color and underline setAttributes() calls, to make things clear, the same happens when it's one call.
let text = "call "
let phone = "1800-800-900"
let attrString = NSMutableAttributedString(string: text + phone, attributes: nil)
let rangeText = (attrString.string as NSString).range(of: text)
let rangePhone = (attrString.string as NSString).range(of: phone)
attrString.setAttributes([NSAttributedStringKey.foregroundColor: UIColor.red],
range: rangeText)
attrString.setAttributes([NSAttributedStringKey.foregroundColor: UIColor.blue],
range: rangePhone)
attrString.setAttributes([NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue],
range: rangePhone)
From the doc of setAttributes():
These new attributes replace any attributes previously associated with
the characters in aRange.
So in other words, it replace them, erasing all previously set, so when you add the underline, it removes the color at that range.
Solution, use addAttributes() instead of setAttributes():
let text = "call "
let phone = "1800-800-900"
let attrString = NSMutableAttributedString(string: text + phone, attributes: nil)
let rangeText = (attrString.string as NSString).range(of: text)
let rangePhone = (attrString.string as NSString).range(of: phone)
attrString.addAttributes([NSAttributedStringKey.foregroundColor: UIColor.red],
range: rangeText)
attrString.addAttributes([NSAttributedStringKey.foregroundColor: UIColor.blue],
range: rangePhone)
attrString.addAttributes([NSAttributedStringKey.underlineStyle: NSUnderlineStyle.styleSingle.rawValue],
range: rangePhone)
Other solution, use two NSAttributedString (I also remove the NSAttributedStringKey in the enum)
let textAttrStr = NSAttributedString(string:text, attributes:[.foregroundColor: UIColor.red])
let phoneAttrStr = NSAttributedString(string:phone, attributes:[.foregroundColor: UIColor.blue,
.underlineStyle: NSUnderlineStyle.styleSingle.rawValue])
let finalAttrStr = NSMutableAttributedString.init(attributedString: textAttrStr)
finalAttrStr.append(phoneAttrStr)
Possible issue with the first solution:
range(of:) returns the range of the first occurence only.
In other words, if text = "1800 " and phone = "18", you'll get unwanted results. because rangePhone would be from index 0 to 1, and not 7 to 8 in 1800 18. This issue won't happen in the second one.
You should not separate the second and the third call of setAttributes, since the latter will overwrite the earlier. Combine the styles in one array:
attrString.setAttributes([.foregroundColor: UIColor.blue,
.underlineStyle: NSUnderlineStyle.styleSingle.rawValue],
range: rangePhone)
Result:

I need to remove this using encoding or decoding or replacement to remove \\U00b4 from string

I am getting I\\U00b4m in place of I'm. How to decode or replace this \\U00b4 from string ?
I\\U00b4m looking for the only one man in my life.
Easy way:
let aString = "This is my string"
let newString = aString.replacingOccurrences(of: "\U00b4", with: "", options: .literal, range: nil)
More info in here: Any way to replace characters on Swift String?