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
Related
I get different text from API and I want change text color for every 5 first word. I try use range and attributes string, but I do something wrong and this not good work for me. How can i do it?
this is my code:
private func setMessageText(text: String) {
let components = text.components(separatedBy: .whitespacesAndNewlines)
let words = components.filter { !$0.isEmpty }
if words.count >= 5 {
let attribute = NSMutableAttributedString.init(string: text)
var index = 0
for word in words where index < 5 {
let range = (text as NSString).range(of: word, options: .caseInsensitive)
attribute.addAttribute(NSAttributedString.Key.foregroundColor, value: Colors.TitleColor, range: range)
attribute.addAttribute(NSAttributedString.Key.font, value: Fonts.robotoBold14, range: range)
index += 1
}
label.attributedText = attribute
} else {
label.text = text
}
}
enter image description here
It's more efficient to get the index of the end of the 5th word and add color and font once for the entire range.
And you are strongly discouraged from bridging String to NSString to get a subrange from a string. Don't do that. Use native Swift Range<String.Index>, there is a convenience API to convert Range<String.Index> to NSRange reliably.
private func setMessageText(text: String) {
let components = text.components(separatedBy: .whitespacesAndNewlines)
let words = components.filter { !$0.isEmpty }
if words.count >= 5 {
let endOf5thWordIndex = text.range(of: words[4])!.upperBound
let nsRange = NSRange(text.startIndex..<endOf5thWordIndex, in: text)
let attributedString = NSMutableAttributedString(string: text)
attributedString.addAttributes([.foregroundColor : Colors.TitleColor, .font : Fonts.robotoBold14], range: nsRange)
label.attributedText = attributedString
} else {
label.text = text
}
}
An alternative – more sophisticated – way is to use the dedicated API enumerateSubstrings(in:options: with option byWords
func setMessageText(text: String) {
var wordIndex = 0
var attributedString : NSMutableAttributedString?
text.enumerateSubstrings(in: text.startIndex..., options: .byWords) { (substring, substringRange, enclosingRange, stop) in
if wordIndex == 4 {
let endIndex = substringRange.upperBound
let nsRange = NSRange(text.startIndex..<endIndex, in: text)
attributedString = NSMutableAttributedString(string: text)
attributedString!.addAttributes([.foregroundColor : Colors.TitleColor, .font : Fonts.robotoBold14], range: nsRange)
stop = true
}
wordIndex += 1
}
if let attributedText = attributedString {
label.attributedText = attributedText
} else {
label.text = text
}
}
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
I'm trying to apply formatting on the selected range of the textview. The problem is when the selected text format applied, the rest of text reset its format.
Here is my code:
if let text = textView.text {
if let textRange = textView.selectedTextRange {
if let selectedText = textView.text(in: textRange) {
let range = (text as NSString).range(of: selectedText)
let attributedString = NSMutableAttributedString(string:text)
attributedString.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: 18) , range: range)
self.textView.attributedText = attributedString
}
}
}
let range = textView.selectedRange
let string = NSMutableAttributedString(attributedString:
textView.attributedText)
let attributes = [NSForegroundColorAttributeName: UIColor.redColor()]
string.addAttributes(attributes, range: textView.selectedRange)
textView.attributedText = string
textView.selectedRange = range
I'm actually developing a Quran application in which I'm using TextKit to highlight verses and change their color. Everything is going great but I have a little problem with words that appear multiple times. First of all, my code is:
import UIKit
class ViewController: UIViewController {
let attributedBackgroundColor = [ NSBackgroundColorAttributeName: UIColor.lightGray ]
var myVerses = ["بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ","الْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ","الرَّحْمَنِ الرَّحِيمِ","مَالِكِ يَوْمِ الدِّينِ","إِيَّاكَ نَعْبُدُ وَإِيَّاكَ نَسْتَعِينُ","اهدِنَا الصِّرَاطَ الْمُسْتَقِيمَ","صِرَاطَ الَّذِينَ أَنْعَمْتَ عَلَيْهِمْ غَيْرِ الْمَغْضُوبِ عَلَيْهِمْ وَلاَ الضَّالِّينَ"]
#IBOutlet weak var textView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
let string = NSMutableAttributedString(string: "Vide initialement")
string.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: CGFloat(25.0)), range: NSRange(location: 0, length: string.length))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
string.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSRange(location: 0, length: string.length))
textView.attributedText = string
let singleTap = UITapGestureRecognizer(target: self, action: #selector(ViewController.tapRecognized))
singleTap.numberOfTapsRequired = 1
textView.addGestureRecognizer(singleTap)
textView.isEditable = false
textView.isSelectable = false
var str = ""
// Ajouter numérotation aux verses - Add numerotation to verses.
for i in 0..<myVerses.count {
if i > 0 {
str += "("+"\(i)"+")"
}
str += String(myVerses[i])
}
print(str)
textView.text = str + "("+"\(myVerses.count)"+")"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// Sélection des verses - Select Verses
func tapRecognized(_ recognizer: UITapGestureRecognizer) {
if recognizer.state == .recognized {
let point = recognizer.location(in: recognizer.view)
let detectedText = self.getWordAtPosition(pos: point, in: textView)
if (detectedText != "") {
print("detectedText == \(detectedText)")
let string = NSMutableAttributedString(string: self.textView.text)
let verses = self.textView.text.components(separatedBy: ")")
if let detectedRange = textView.text.range(of: detectedText) {
let startPosOfSubstring = textView.text.distance(from: textView.text.startIndex, to: detectedRange.lowerBound)
let detectedLength = detectedText.characters.count
let rangeOfSub = (startPosOfSubstring,detectedLength)
print("-- rangeofSub == " ,rangeOfSub)
let rangeOfSubstring = NSRange(location: startPosOfSubstring, length: detectedLength)
for verse: String in verses {
if let detectedVerse = textView.text.range(of: verse) {
let startPosOfVerse = textView.text.distance(from: textView.text.startIndex, to: detectedVerse.lowerBound)
let detectedLengthOfVerse = verse.characters.count
let tupleVerse = (startPosOfVerse,detectedLengthOfVerse)
print("++ rangeofVerse == " ,tupleVerse)
let rangeOfVerse = NSRange(location: startPosOfVerse, length: detectedLengthOfVerse)
print(verse)
let range = (self.textView.text as NSString).range(of: verse)
let contained = NSLocationInRange(rangeOfSubstring.location, rangeOfVerse)
if (contained) {
print ("************************************")
print("contained is :" ,contained)
print ("************************************")
string.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: range)
string.addAttribute(NSBackgroundColorAttributeName, value: UIColor.darkGray, range: range)
string.addAttribute(NSFontAttributeName, value: UIFont.systemFont(ofSize: CGFloat(25.0)), range: NSRange(location: 0, length: string.length))
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
string.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSRange(location: 0, length: string.length))
}
print ("--------------------------------------------")
}
}
self.textView.attributedText = string
} else {
print("detectedText is empty")
}
}
}
}
func getWordAtPosition( pos: CGPoint, in textview: UITextView) -> String {
//Eleminer le balancement du scroll - eliminate scroll offset
// var pos = pos
// pos.y += tv.contentOffset.y
//Position du text tapé au point - get location in text from textposition at point
let tapPos = textview.closestPosition(to: pos)
//Avoir le mot tapé dans la position du point - fetch the word at this position (or nil, if not available)
if let wr = textview.tokenizer.rangeEnclosingPosition(tapPos!, with: .word, inDirection: UITextLayoutDirection.right.rawValue) {
print(pos)
return textview.text(in: wr)!
}else{
return ""
}
}
}
When I run my app on the simulator, I select a verse and it's going great.
Normal Use:
The problem is with words that appear more than once. For example, for the word "الرحمن" which is found twice, regardless of whether I select the first or the second one, the verse that gets highlighted is always the first one.
What am I doing wrong?
Thanks in advance.
As I understand your question, you want to highlight the entire verse when the user selects a particular word in that verse.
The problem in your logic is that you determine the word that the user has selected, but that word is not necessarily unique, so when you search for this word (with textView.text.range(of: detectedText)), you get back the first result, which may or may not be the one the user actually selected.
One potential solution, if the tokenizer is correctly understanding your text, is to use a different TextGranularity when determining the user's selection. Instead of:
textview.tokenizer.rangeEnclosingPosition(tapPos!, with: .word, inDirection: UITextLayoutDirection.right.rawValue)
you could try:
textview.tokenizer.rangeEnclosingPosition(tapPos!, with: .sentence, inDirection: UITextLayoutDirection.right.rawValue)
How do you change the color of specific texts within an array of string that's going to be passed into a label?
Let's say I have an array of string:
var stringData = ["First one", "Please change the color", "don't change me"]
And then it's passed to some labels:
Label1.text = stringData[0]
Label2.text = stringData[1]
Label3.text = stringData[2]
What's the best approach to change the color of the word "the" in stringData[1]?
Thank you in advance for your help!
let str = NSMutableAttributedString(string: "Please change the color")
str.addAttributes([NSForegroundColorAttributeName: UIColor.red], range: NSMakeRange(14, 3))
label.attributedText = str
The range is the range of the specific text.
If you want to change the color of all the in your string:
func highlight(word: String, in str: String, with color: UIColor) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: str)
let highlightAttributes = [NSForegroundColorAttributeName: color]
let nsstr = str as NSString
var searchRange = NSMakeRange(0, nsstr.length)
while true {
let foundRange = nsstr.range(of: word, options: [], range: searchRange)
if foundRange.location == NSNotFound {
break
}
attributedString.setAttributes(highlightAttributes, range: foundRange)
let newLocation = foundRange.location + foundRange.length
let newLength = nsstr.length - newLocation
searchRange = NSMakeRange(newLocation, newLength)
}
return attributedString
}
label2.attributedText = highlight(word: "the", in: stringData[1], with: .red)