How can we get the line range before another line range? - swift

I use this code to get the current line range:
extension UITextView {
var currentLineRange: NSRange {
let nsText = self.text as NSString
let currentRange = nsText.lineRange(for: self.selectedRange)
return currentRange
}
}
How can I get the line range before another one, like :
func getLineRangeBefore(_ lineRange: NSRange) -> NSRange {
//...
}

One way to do this is to get the line start of the range, then find the line range of the position before the line start:
extension NSString {
func getLineRangeBefore(_ lineRange: NSRange) -> NSRange? {
var lineStart = 0
getLineStart(&lineStart, end: nil, contentsEnd: nil, for: lineRange)
if lineStart == 0 {
return nil
}
return self.lineRange(for: NSRange(location: lineStart - 1, length: 0))
}
}
Note that this should return an optional NSRange because the range could be contained by the first line, in which case there is no line before it.

Related

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"]
)

How I can change text color for every 5 first words in label?

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
}
}

Change the style of an array of String inside a TextView - swift - EDIT

My first question was how to change the Font of a word example "test" in a textView, and it was answered correctly by #bkrl and #Torongo.
func changeAllOccurence(of string: String, font: UIFont) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: self)
var range = NSMakeRange(0, attributedString.mutableString.length)
while(range.location != NSNotFound)
{
range = attributedString.mutableString.range(of: string, options: .caseInsensitive, range: range)
if(range.location != NSNotFound)
{
attributedString.addAttribute(NSFontAttributeName,
value: font,
range: range)
range = NSMakeRange(range.location + range.length, self.characters.count - (range.location + range.length));
}
}
return attributedString
}
As I am still not familiar with the above code, I tried to add few lines in order to generalize the code so it can works for an array of strings and not only one string.
But for sure it didn't works cause it changed the font of the last word only which is reasonable cause the final changes will be for the last word which is "usage":
let words = ["example", "usage"]
for word in words {
let attributedText = text.changeAllOccurence(of: word, font: UIFont.boldSystemFont(ofSize: 17))
textview.attributedText = attributedText
}
Can someone advise how to improve the code provided by #Toromgo in order to work for any array of strings instead of just one?
You could create extension like:
extension String {
func change(font: UIFont, of string: String) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: self)
let subStringRange = attributedString.mutableString.range(of: string,
options: .caseInsensitive)
if subStringRange.location != NSNotFound {
attributedString.addAttribute(NSFontAttributeName,
value: font,
range: subStringRange)
}
return attributedString
}
}
Usage:
let text = "This is example of usage extension"
let attributedText = text.change(font: UIFont.boldSystemFont(ofSize: 17), of: "example")
textView.attributedText = attributedText
Hope that helps!
As you have changed your question, ihave updated my answer accordingly. Please try this:
extension String {
func changeAllOccurence(of strings: [String], font: UIFont) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: self)
for eachString in strings {
var range = NSMakeRange(0, attributedString.mutableString.length)
while(range.location != NSNotFound)
{
range = attributedString.mutableString.range(of: eachString, options: .caseInsensitive, range: range)
if(range.location != NSNotFound)
{
attributedString.addAttribute(NSFontAttributeName,
value: font,
range: range)
range = NSMakeRange(range.location + range.length, self.characters.count - (range.location + range.length));
}
}
}
return attributedString
}
}
I have run the code and it is working.

How to make auto numbering on UITextview when press return key in swift

When user press [Return] key then need to display the number. Like serial numbers [1,2 etc] for each line.Is there any way to do that?
Following code I tried
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
var textview_text_nssring = textView.text as NSString
// Add "1" when the user starts typing into the text field
if (range.location == 0 && textView.text.isEmpty ) {
if text == "\n" {
textView.text = "1."
let cursor = NSMakeRange(range.location + 3, 0)
textView.selectedRange = cursor
return false
}
else {
textView.text = NSString(format: "1. %#", text) as String
}
}
return true
}
Declare one Int var for current line number and use it inside shouldChangeTextIn range like this way.
var currentLine: Int = 1
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
// Add "1" when the user starts typing into the text field
if (textView.text.isEmpty && !text.isEmpty) {
textView.text = "\(currentLine). "
currentLine += 1
}
else {
if text.isEmpty {
if textView.text.characters.count >= 4 {
let str = textView.text.substring(from:textView.text.index(textView.text.endIndex, offsetBy: -4))
if str.hasPrefix("\n") {
textView.text = String(textView.text.characters.dropLast(3))
currentLine -= 1
}
}
else if text.isEmpty && textView.text.characters.count == 3 {
textView.text = String(textView.text.characters.dropLast(3))
currentLine = 1
}
}
else {
let str = textView.text.substring(from:textView.text.index(textView.text.endIndex, offsetBy: -1))
if str == "\n" {
textView.text = "\(textView.text!)\(currentLine). "
currentLine += 1
}
}
}
return true
}

How to select a text in a textView having a NSRange in Swift?

I have a UITextView where I would like to select a part of the text. I'm trying to use selectedTextRange, unfortunately I get this error:
Cannot convert value of type 'NSRange' (aka '_NSRange') to expected
argument type 'UITextRange'
This is the code I'm trying to use:
mainTextField.becomeFirstResponder()
mainTextField.selectedTextRange = mainTextField.textInRange(matchRange) // matchRange is a NSRange
The range I'm using is coming from a Regex which I adapted from this tutorial1:
EDIT: added the first line
if let regex = NSRegularExpression(options: searchOptions){ // refers to an installed extension convenience init
let range = NSMakeRange(0, (mainTextField.text.characters.count))
let matches = regex.matchesInString(mainTextField.text, options: [], range: range)
[...]
You can add an extension on UITextView that will do the conversion.
extension UITextView
{
func textRangeFromNSRange(range:NSRange) -> UITextRange?
{
let beginning = self.beginningOfDocument
guard let start = self.positionFromPosition(beginning, offset: range.location), end = self.positionFromPosition(start, offset: range.length) else { return nil}
return self.textRangeFromPosition(start, toPosition: end)
}
}
Use
if let range = mainTextView.textRangeFromNSRange(range: matchRange){
mainTextField.selectedTextRange = range
}
Where matchRange is an NSRange and range is a UITextRange
Swift 4 updated version of #beyowulf's answer:
extension UITextView {
func textRangeFromNSRange(range:NSRange) -> UITextRange? {
let beginning = beginningOfDocument
guard let start = position(from: beginning, offset: range.location), let end = position(from: start, offset: range.length) else { return nil }
return textRange(from: start, to: end)
}
}
Just to add a little more information, I found also an alternative way: instead of using selectedTextRange and doing the conversion, you can use selectedRange.
Note: matchRange is a NSRange
// this don't work because selectedTextRange requires an UITextRange
textField.selectedTextRange = textField.textInRange(matchRange) // don't work
// this works because selectedRange requires a NSRange
textField.selectedRange = matchRange // works