swift bold and italic between delimiters in attributed string - swift

I need to convert a string with some delimiters to an attributed string.
My delimiters are $$italic$$ and $$$bold$$$. Actually, my code is functionnal only if I do not combine italic and bold. My trouble is $$$$$bold AND italic$$$$$. Only italic is set...
Here is my code in the UIViewcontroller Class :
func pikipiki2AttributedString(text: String) -> NSAttributedString {
let attributedText = NSMutableAttributedString(string: text)
let size = [NSFontAttributeName : UIFont.systemFontOfSize(15.0)]
attributedText.addAttributes(size, range: NSRange(location: 0, length: attributedText.length))
let attributeBold = [NSFontAttributeName: UIFont.boldSystemFontOfSize(15)]
try! attributedText.addAttributes(attributeBold, delimiter: "$$$")
let attributeItalic = [NSFontAttributeName: UIFont.italicSystemFontOfSize(15)]
try! attributedText.addAttributes(attributeItalic, delimiter: "$$")
return attributedText
}
And an extension founded here :
public extension NSMutableAttributedString {
func addAttributes(attrs: [String : AnyObject], delimiter: String) throws {
let escaped = NSRegularExpression.escapedPatternForString(delimiter)
let regex = try NSRegularExpression(pattern:"\(escaped)(.*?)\(escaped)", options: [])
var offset = 0
regex.enumerateMatchesInString(string, options: [], range: NSRange(location: 0, length: string.characters.count)) { (result, flags, stop) -> Void in
guard let result = result else {
return
}
let range = NSRange(location: result.range.location + offset, length: result.range.length)enter code here
self.addAttributes(attrs, range: range)
let replacement = regex.replacementStringForResult(result, inString: self.string, offset: offset, template: "$1")
self.replaceCharactersInRange(range, withString: replacement)
offset -= (2 * delimiter.characters.count)
}
}
}

Related

Applying NSMutableAttributedString to a range of text

I have some text:
New Content - Published Today | 10 min read
I'd like to apply styles to everything after and including the pipe, so | 10 min read
I have tried the below but it has only styles the pipe itself.
func makeAttributedText(using baseString: String?) -> NSMutableAttributedString? {
guard let baseString = baseString else { return nil }
let attributedString = NSMutableAttributedString(string: baseString, attributes: nil)
let timeToReadRange = (attributedString.string as NSString).range(of: "|")
attributedString.setAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 18)], range: timeToReadRange)
return attributedString
}
Rather than getting the range of a single character get the index of the character and create a range from that index to the end of the string.
func makeAttributedText(using baseString: String?) -> NSMutableAttributedString? {
guard let baseString = baseString else { return nil }
let attributedString = NSMutableAttributedString(string: baseString, attributes: nil)
guard let timeToReadIndex = baseString.firstIndex(of: "|") else { return attributedString }
let timeToReadRange = NSRange(timeToReadIndex..., in: baseString)
attributedString.setAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 18)], range: timeToReadRange)
return attributedString
}
Note:
Swift has dedicated methods to convert Range<String.Index> to NSRange. There's no reason for the bridge cast to NSString

Color a string based on occurrences in a Variable String

Im trying to create a function which will allow an input string to have certain words coloured and then the function wouldn return a coloured string.
I started with the red colour first but can’t figure out how to make it work.
My code so far:
let oldString = "TEST STRING TO COLOUR IT WORDS EXIST" //sample of a variable string that may or may not contain wors that need coloring
let newString = stringColorCoding(stringToColor: oldString, colorRed: "TO, POT, TEST", colorYellow: "EXIST, TOP", colorGreen: "AB, +TA, -XY, WORDS")
func stringColorCoding(stringToColor: String, colorRed: String, colorYellow: String, colorGreen: String)
{
let attrStr = NSMutableAttributedString(string: stringToColor)
let inputLength = attrStr.string.count
let searchStringRed = colorRed
let searchLengthRed = searchStringRed.characters.count
var rangeRed = NSRange(location: 0, length: attrStr.length)
while (range.location != NSNotFound)
{
range = (attrStr.string as NSString).range(of: searchStringRed, options: [], range: range)
if (range.location != NSNotFound)
{
attrStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.systemRed, range: NSRange(location: range.location, length: searchLengthRed))
range = NSRange(location: range.location + range.length, length: inputLength - (range.location + range.length))
}
}
return attrStr
}
The way you are going about it is quite complicated. I'd use enumerateSubstrings to get each word in the string. And then instead of passing in a comma-separated string with search terms I'd pass in an array of search strings.
extension String {
func highlighted(
redText: [String],
yellowText: [String],
greenText: [String]
) -> NSAttributedString {
let result = NSMutableAttributedString(string: self)
enumerateSubstrings(in: startIndex..<endIndex, options: .byWords) {
(substring, substringRange, _, _) in
guard let substring = substring else { return }
if redText.contains(substring) {
result.addAttribute(
.foregroundColor,
value: UIColor.systemRed,
range: NSRange(substringRange, in: self)
)
}
if yellowText.contains(substring) {
result.addAttribute(
.foregroundColor,
value: UIColor.systemYellow,
range: NSRange(substringRange, in: self)
)
}
if greenText.contains(substring) {
result.addAttribute(
.foregroundColor,
value: UIColor.systemGreen,
range: NSRange(substringRange, in: self)
)
}
}
return result
}
}
The usage is as follows:
let highlighted = "TEST TO COLOUR IT WORDS EXIST".highlighted(
redText: ["TO", "POT", "TEST"],
yellowText: ["EXIST", "TOP"],
greenText: ["AB", "+TA", "-XY", "WORDS"]
)

I need help removing underscore after formatting a string

When a string starts and ends with an underscore, I am making that string italic. After that, I am removing the underscores. This works fine if the string is like this "_hello_ world"
However, this doesn't work => "_hello_ world _happy_"
This is my regex => "\\_(.*?)\\_"
func applyItalicFormat(string: String) {
let matches = RegexPattern.italicRegex.matches(string)
for match in matches {
let mRange = match.range
self.addAttributes([NSAttributedStringKey.font : UIFont.latoMediumItalic(size: 15)],
range: mRange)
if let rangeObj = Range(NSMakeRange(mRange.location, mRange.upperBound), in: string) {
var sub = string.substring(with: rangeObj)
sub = sub.replacingOccurrences(of: "_", with: "")
print("sub is \(sub)")
replaceCharacters(in: mRange, with: sub)
} else {
}
}
}
Another Regex format, \\_(?:(?!_).)+\\_ and using map
var mySentence = "This is from _mcdonal_ mac _system_ which says that _below_ answer is one of the _easiest_ way"
var wholeText = NSMutableAttributedString()
override func viewDidLoad() {
super.viewDidLoad()
wholeText = NSMutableAttributedString(string: mySentence)
italicLbl.attributedText = matches(for: "\\_(?:(?!_).)+\\_", in: mySentence)
}
func matches(for regex: String, in text: String) -> NSAttributedString {
do {
let regex = try NSRegularExpression(pattern: regex)
let results = regex.matches(in: text, range: NSRange(text.startIndex..., in: text))
let _ = results.map { (val) in
wholeText.addAttributes([NSAttributedString.Key.font : UIFont.italicSystemFont(ofSize: 17)],
range: val.range)
var sub = String(text[Range(val.range, in: text)!])
sub = sub.replacingOccurrences(of: "_", with: " ")
wholeText.replaceCharacters(in: val.range, with: sub)
}
return wholeText
} catch let error {
print("invalid regex: \(error.localizedDescription)")
return wholeText
}
}
Screenshot
With small modifications, and use of range(at:) of the matches:
extension NSMutableAttributedString {
func applyItalicFormat(pattern: String) {
let regex = try! NSRegularExpression(pattern: pattern, options: [])
let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count))
let italicAttributes = [NSAttributedString.Key.font: UIFont.italicSystemFont(ofSize: 15)]
for match in matches.reversed() {
let textRange = match.range(at: 1)
let attributedTextToChange = NSMutableAttributedString(attributedString: self.attributedSubstring(from: textRange))
attributedTextToChange.addAttributes(italicAttributes, range: NSRange(location: 0, length: attributedTextToChange.length))
replaceCharacters(in: match.range, with: attributedTextToChange)
}
}
}
You don't need to replace the _, you have already the good range of the text alone without the underscores.
I use the matches.reversed(), because when you apply the first one, then the range of the second already found is not correct anymore (you remove twice _).
I prefer to extract the attributedString part to modify, modify it, and then replace it with the modified it.
I simplified some rest of the code.
Sample Test (usable in Playground):
let initialTexts = ["_hello_ world", "\n\n", "_hello_ world _happy_"]
let label = UILabel.init(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
label.backgroundColor = .orange
label.numberOfLines = 0
let attr = NSMutableAttributedString()
for anInitialText in initialTexts {
let attributedStr = NSMutableAttributedString(string: anInitialText)
attributedStr.applyItalicFormat(pattern: "\\_(.*?)\\_")
attr.append(attributedStr)
}
label.attributedText = attr

UITextView Attributed text with 2 links, each with different colors not working

I want to display in an attributed string 2 links, each link with a different color. I do not understand how to do that. It will always set just one color. I've been struggling with this for days and still can't figure out how to make it work. Does anybody know? I can set two colors but not for links! All links are the same color.
This is my whole implementation: (UPDATE)
var checkIn = ""
var friends = ""
//MARK: Change Name Color / Font / Add a second LABEL into the same label
func setColorAndFontAttributesToNameAndCheckIn() {
let nameSurname = "\(postAddSetup.nameSurname.text!)"
checkIn = ""
friends = ""
if selectedFriends.count == 0 {
print("we have no friends...")
friends = ""
} else if selectedFriends.count == 1 {
print("we have only one friend...")
friends = ""
friends = " is with \(self.firstFriendToShow)"
} else if selectedFriends.count > 1 {
print("we have more than one friend...")
friends = ""
friends = " is with \(self.firstFriendToShow) and \(self.numberOfFriendsCount) more"
}
if checkIn == "" {
checkIn = ""
}
var string = postAddSetup.nameSurname.text
string = "\(nameSurname)\(friends)\(checkIn) "
let attributedString = NSMutableAttributedString(string: string!)
attributedString.addAttribute(NSFontAttributeName, value: UIFont.boldSystemFont(ofSize: 14), range: (string! as NSString).range(of: nameSurname))
attributedString.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 13), range: (string! as NSString).range(of: checkIn))
attributedString.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 13), range: (string! as NSString).range(of: friends))
attributedString.addLink("checkIn", linkColor: UIColor.darkGray, text: checkIn)
attributedString.addLink("tagFriends", linkColor: UIColor.red, text: friends)
//attributedString.addAttribute(NSLinkAttributeName, value: "checkIn", range: (string! as NSString).range(of: checkIn))
//attributedString.addAttribute(NSLinkAttributeName, value: "tagFriends", range: (string! as NSString).range(of: friends))
//postAddSetup.nameSurname.linkTextAttributes = [NSForegroundColorAttributeName:UIColor.redIWorkOut(), NSFontAttributeName: UIFont.systemFont(ofSize: 13)]
//attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.darkGray, range: (string! as NSString).range(of: checkIn))
postAddSetup.nameSurname.attributedText = attributedString
print("atribute: \(attributedString)")
}
func string1Action() {
print("action for string 1...")
}
func string2Action() {
print("action for string 2...")
}
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if URL.absoluteString == "string1" {
string1Action()
} else if URL.absoluteString == "string2" {
string2Action()
}
return false
}
extension NSMutableAttributedString {
func addLink(_ link: String, linkColor: UIColor, text: String) {
let pattern = "(\(text))"
let regex = try! NSRegularExpression(pattern: pattern,
options: NSRegularExpression.Options(rawValue: 0))
let matchResults = regex.matches(in: self.string,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSRange(location: 0, length: self.string.characters.count))
for result in matchResults {
self.addAttribute(NSLinkAttributeName, value: link, range: result.rangeAt(0))
self.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: result.rangeAt(0))
}
}
}
I have used in a project this NSMutableAttributedString extension adapted from this Article.
Using NSRegularExpression you can assign your respective color matching the range of your link text:
The extension:
extension NSMutableAttributedString {
func addLink(_ link: String, linkColor: UIColor, text: String) {
let pattern = "(\(text))"
let regex = try! NSRegularExpression(pattern: pattern,
options: NSRegularExpression.Options(rawValue: 0))
let matchResults = regex.matches(in: self.string,
options: NSRegularExpression.MatchingOptions(rawValue: 0),
range: NSRange(location: 0, length: self.string.characters.count))
for result in matchResults {
self.addAttribute(NSLinkAttributeName, value: link, range: result.rangeAt(0))
self.addAttribute(NSForegroundColorAttributeName, value: linkColor, range: result.rangeAt(0))
}
}
}
Edit:
Set a custom UITextView class to use this extension and using the delegate function shouldInteractWith url it’s possible to simulate the hyperlink logic of UITextView:
class CustomTextView: UITextView {
private let linksAttributes = [NSLinkAttributeName]
override func awakeFromNib() {
super.awakeFromNib()
let tapGest = UITapGestureRecognizer(target: self, action: #selector(self.onTapAction))
self.addGestureRecognizer(tapGest)
}
#objc private func onTapAction(_ tapGest: UITapGestureRecognizer) {
let location = tapGest.location(in: self)
let charIndex = self.layoutManager.characterIndex(for: location, in: self.textContainer, fractionOfDistanceBetweenInsertionPoints: nil)
if charIndex < self.textStorage.length {
var range = NSMakeRange(0, 0)
for linkAttribute in linksAttributes {
if let link = self.attributedText.attribute(linkAttribute, at: charIndex, effectiveRange: &range) as? String {
guard let url = URL(string: link) else { return }
_ = self.delegate?.textView?(self, shouldInteractWith: url, in: range, interaction: .invokeDefaultAction)
}
}
}
}
}
How to use:
attributedString.addLink(yourLinkUrl, linkColor: yourLinkColor, text: yourLinkText)
let textView = CustomTextView()
textView.attributedText = attributedString

UITextView change text color of specific text

I want to change the text color of a specific text within a UITextView which matches an index of an array. I was able to slightly modify this answer but unfortunatly the text color of each matching phrase is only changed once.
var chordsArray = ["Cmaj", "Bbmaj7"]
func getColoredText(textView: UITextView) -> NSMutableAttributedString {
let text = textView.text
let string:NSMutableAttributedString = NSMutableAttributedString(string: text)
let words:[String] = text.componentsSeparatedByString(" ")
for word in words {
if (chordsArray.contains(word)) {
let range:NSRange = (string.string as NSString).rangeOfString(word)
string.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: range)
}
}
chords.attributedText = string
return string
}
Outcome
In case, someone needs it in swift 4. This is what I get from my Xcode 9 playground :).
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController
{
override func loadView()
{
let view = UIView()
view.backgroundColor = .white
let textView = UITextView()
textView.frame = CGRect(x: 150, y: 200, width: 200, height: 20)
textView.text = "#Kam #Jam #Tam #Ham"
textView.textColor = .black
view.addSubview(textView)
self.view = view
let query = "#"
if let str = textView.text {
let text = NSMutableAttributedString(string: str)
var searchRange = str.startIndex..<str.endIndex
while let range = str.range(of: query, options: NSString.CompareOptions.caseInsensitive, range: searchRange) {
text.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.gray, range: NSRange(range, in: str))
searchRange = range.upperBound..<searchRange.upperBound
}
textView.attributedText = text
}
}
}
PlaygroundPage.current.liveView = MyViewController()
I think for swift 3, you need to convert Range(String.Index) to NSRange manually like this.
let start = str.distance(from: str.startIndex, to: range.lowerBound)
let len = str.distance(from: range.lowerBound, to: range.upperBound)
let nsrange = NSMakeRange(start, len)
text.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.gray, range: nsrange)
Swift 4.2 and 5
let string = "* Your receipt photo was not clear or did not capture the entire receipt details. See our tips here.\n* Your receipt is not from an eligible grocery, convenience or club store."
let attributedString = NSMutableAttributedString.init(string: string)
let range = (string as NSString).range(of: "See our tips")
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.blue, range: range)
txtView.attributedText = attributedString
txtView.isUserInteractionEnabled = true
txtView.isEditable = false
Output
Sorry, I just noticed your message. Here is a working example (tested in a playground):
import UIKit
func apply (string: NSMutableAttributedString, word: String) -> NSMutableAttributedString {
let range = (string.string as NSString).rangeOfString(word)
return apply(string, word: word, range: range, last: range)
}
func apply (string: NSMutableAttributedString, word: String, range: NSRange, last: NSRange) -> NSMutableAttributedString {
if range.location != NSNotFound {
string.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: range)
let start = last.location + last.length
let end = string.string.characters.count - start
let stringRange = NSRange(location: start, length: end)
let newRange = (string.string as NSString).rangeOfString(word, options: [], range: stringRange)
apply(string, word: word, range: newRange, last: range)
}
return string
}
var chordsArray = ["Cmaj", "Bbmaj7"]
var text = "Cmaj Bbmaj7 I Love Swift Cmaj Bbmaj7 Swift"
var newText = NSMutableAttributedString(string: text)
for word in chordsArray {
newText = apply(newText, word: word)
}
newText