I have arrays containing strings of the text terms to which I want to apply a particular attribute. Here's a code snippit:
static var bold = [String]()
static let boldAttribs = [NSFontAttributeName: UIFont(name: "WorkSans-Medium", size: 19)!]
for term in bold {
atStr.addAttributes(boldAttribs, range: string.rangeOfString(term))
}
This works great for single term or phrase use. But it only applies to the first use of a specific term. Is there a way, without resorting to numerical ranges, to apply the attribute to all instances of the same term? For example, make every use of "animation button" within the same string bold.
Edit: This works.
// `butt2` is [String]() of substrings to attribute
// `term` is String element in array, target of attributes
// `string` is complete NAString from data
// `atStr` is final
for term in butt2 {
var pos = NSRange(location: 0, length: string.length)
while true {
let next = string.rangeOfString(term, options: .LiteralSearch, range: pos)
if next.location == NSNotFound { break }
pos = NSRange(location: next.location+next.length, length: string.length-next.location-next.length)
atStr.addAttributes(butt2Attribs, range: next)
}
}
You don't have to resort to numerical ranges, but you do need to resort to a loop:
// atStr is mutable attributed string
// str is the input string
atStr.beginEditing()
var pos = NSRange(location: 0, length: atStr.length)
while true {
let next = atStr.rangeOfString(target, options: .LiteralSearch, range: pos)
if next.location == NSNotFound {
break
}
atStr.addAttributes(boldAttribs, range: next)
print(next)
pos = NSRange(location: next.location+next.length, length: atStr.length-next.location-next.length)
}
atStr.endEditing()
Related
In a NSTextContentStorageDelegate I use textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? to serve custom NSTextParagraphs. In these I set a paragraph style to the attributed string. The head indent of this paragraph style equals the amount of whitespace at the beginning of the string. This leads to an aligned indent when the line is wrapped in the text view.
This works. However, in some cases (not always) the caret will not move to the end of the wrapped line. It is stuck before and does not reflect the correct insertion point, a bit like so:
xxx xx xx
xxx xx Ixx <- does not move to the last characters
I add the code below. Does anybody have an idea why this may happen? All help is greatly appreciated.
This is the code:
extension HighlightTextEditor.Coordinator : NSTextContentStorageDelegate{
func textContentStorage(_ textContentStorage: NSTextContentStorage, textParagraphWith range: NSRange) -> NSTextParagraph? {
var paragraphWithDisplayAttributes: NSTextParagraph? = nil
let textWithDisplayAttributes = textContentStorage.textStorage!.attributedSubstring(from: range)
var finalString: NSMutableAttributedString? = nil
if parent.lineBreakStrategy != .none{
var indentLength: CGFloat = 0
//find the first index which is not a whitespace
if let endIndex = textWithDisplayAttributes.string.firstIndex(where: {!$0.isWhitespace}){
if endIndex != textWithDisplayAttributes.string.startIndex{
//calculate a range from start to first non-whitespace character and convert to NSRange
let range: Range = textWithDisplayAttributes.string.startIndex..<endIndex
let convertedRange = NSRange(range, in: textWithDisplayAttributes.string)
//get a substring with only the whitespace characters
var measureString: NSAttributedString?
if parent.lineBreakStrategy == .aligned{
measureString = textWithDisplayAttributes.attributedSubstring(from: convertedRange)
} else if parent.lineBreakStrategy == .indented{
measureString = textWithDisplayAttributes.attributedSubstring(from: convertedRange) + NSAttributedString(string: " ")
}
//get the size of the substring and calcualte the index lengths depending on the strategy
indentLength = measureString?.size().width ?? 0
//create the paragraphStyle and set the headIndent either to the length or 0, if not calculated
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.headIndent = indentLength
//add the paragraphstyle to textWithDisplayAttributes
finalString = NSMutableAttributedString(attributedString: textWithDisplayAttributes)
if let finalString = finalString{
finalString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: finalString.length))
//create the new NSTextPara with the final string
paragraphWithDisplayAttributes = NSTextParagraph(attributedString: finalString)
}
}
}
}
return paragraphWithDisplayAttributes
}
I'm attempting to check whether a word is in the dictionary with the following function
func isReal(word: String) -> Bool {
let checker = UITextChecker()
let range = NSRange(location: 0, length: word.utf16.count)
let wordRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
return wordRange.location == NSNotFound
}
The problem is that this only works correctly for words of seven characters or more. Shorter words return true even if they are not in the dictionary. Specifically, we get wordRange = {9223372036854775807, 0} in this case, the same as for a valid word.
The solution turns out to be embarrassingly simple. Our strings were upper case, and UITextChecker treats any upper case string shorter than seven characters as a possible valid acronym. In lower case everything works as expected.
I find it works just fine, when I use your function in a Swift Playground:
import UIKit
func isReal(word: String) -> Bool {
let checker = UITextChecker()
let range = NSRange(location: 0, length: word.utf16.count)
let wordRange = checker.rangeOfMisspelledWord(in: word, range: range, startingAt: 0, wrap: false, language: "en")
return wordRange.location == NSNotFound
}
let validStrings = ["test", "fest", "fast"]
let validResults = validStrings.map{ isReal(word:$0) }
print(validResults)
let invalidStrings = ["xt", "fxxx", "srwe"]
let invalidResults = invalidStrings.map{ isReal(word:$0) }
print(invalidResults)
Your issue may be platform or version specific.
Attempting to create a function that uses regex matches to return an array of NSRange values to use with a UITextView to allow the user to click through the matched words using animation.
I assume the solution is to break out of the function if there is no regex match. I cannot figure out how to do this where the function requires a NSRange value.
Moreover, when there is no match, the regex function matches does not return nil. Instead, it automatically returns an empty array which appears to make the guard statement useless.
Here is the function:
func rangeOfSearchText(searchString: String, UIText: String) -> [NSRange] {
var matches:[NSTextCheckingResult]?
let regex = try! NSRegularExpression(pattern: searchString, options: .caseInsensitive)
matches = regex.matches(in: UIText, options: [], range: NSRange(location: 0, length: UIText.count))
guard let find = matches else {
//return need to find a way to break out of function if nil without returning an NSRange object...
}
var rangeArray:[NSRange] = []
for match in find {
rangeArray.append(match.range(at: 0))
}
return rangeArray
}
let sString = "z"
let longString = "I need a solution."
let test = rangeOfSearchText(searchString: sString, UIText: longString)
The above returns an empty array.
In a NSAttributed type statement, I want to keep the existing attributed value and give it a new attributed value.
The problem is that replacingOccurrences is only possible for string types, as I want to give a new value every time the word appears in the entire sentence.
If I change NSAttributedString to string type, the attributed value is deleted. I must keep the existing values.
How can I do that?
To get this working,
1. First you need to find the indices of all the duplicate substrings existing in a string. To get that you can use this: https://stackoverflow.com/a/40413665/5716829
extension String {
func indicesOf(string: String) -> [Int] {
var indices = [Int]()
var searchStartIndex = self.startIndex
while searchStartIndex < self.endIndex,
let range = self.range(of: string, range: searchStartIndex..<self.endIndex),
!range.isEmpty
{
let index = distance(from: self.startIndex, to: range.lowerBound)
indices.append(index)
searchStartIndex = range.upperBound
}
return indices
}
}
2. Next you need to apply your desired attribute to substring at each index, i.e.
let str = "The problem is that replacingOccurrences Hello is only possible for string types, as I want to give Hello a new value every time Hello the word appears in the entire sentence Hello."
let indices = str.indicesOf(string: "Hello")
let attrStr = NSMutableAttributedString(string: str, attributes: [.foregroundColor : UIColor.blue])
for index in indices
{
//You can write your own logic to specify the color for each duplicate. I have used some hardcode indices
var color: UIColor
switch index
{
case 41:
color = .orange
case 100:
color = .magenta
case 129:
color = .green
default:
color = .red
}
attrStr.addAttribute(.foregroundColor, value: color, range: NSRange(location: index, length: "Hello".count))
}
Screenshot:
Let me know if you still face any issues. Happy Coding..🙂
You can use replaceCharacters. You can find the range of the substring you want to remove and use it as your range.
I want to make sure that a word is real and this is my code:
var checker: UITextChecker = UITextChecker()
var range: NSRange = NSRange(location: 0,length: (count(completeWord)))
var misspelledRange: NSRange = checker.rangeOfMisspelledWordInString(completeWord, range: range, startingAt: 0, wrap: false, language: "en_US")
var isRealWord: Bool = misspelledRange.location == NSNotFound
if isRealWord {
println("Correct")
} else {
println("Not Correct")
}
But even if I give it a letter, it says correct. What can I do about that? Basically, I want to remove letters from the corrects.
UITextChecker gives you misspelled words in a sentence. It won't show you wrong letter in a word, but wrong word in the whole text input.
For example:
var checker: UITextChecker = UITextChecker()
let string:NSString = "Airplane is gren"
var range: NSRange = NSRange(location: 0,length: string.length)
var misspelledRange: NSRange = checker.rangeOfMisspelledWordInString(string as String, range: range, startingAt: 0, wrap: false, language: "en_US")
misspelledRange.toRange()
gives you result as 12..<16, i.e the whole word.
You could use guessesForWordRange to get possible correct substitutes:
let guesses = checker.guessesForWordRange(misspelledRange, inString: string as String, language: "en_US") as? [String]
(returns ["green", "greg", "grep", "grew", "grey", "gran", "grin", "glen", "aren", "oren", "wren"])