If the text contains emoji, Range is nil from swift - swift

The text "Welcome my application..❣️" does not make sense during the NSRange and Range tests. If ❣️ is included, Range is returned as nil, and I wonder why.
func testA() {
let testStr = "Welcome my application..❣️"
let range = NSRange(location: 0, length: testStr.count)
let wrapRange = Range(range, in: testStr)
let testStrB = "Welcome my application.."
let rangeB = NSRange(location: 0, length: testStrB.count)
let wrapRangeB = Range(rangeB, in: testStrB)
print("wrapRange: \(wrapRange) wrapRangeB: \(wrapRangeB)")
}
RESULT:
wrapRange: nil wrapRangeB: Optional(Range(Swift.String.Index(_rawBits: 1)..<Swift.String.Index(_rawBits: 1572864)))

"❣️" is a single “extended grapheme cluster”, but two UTF-16 code units:
print("❣️".count) // 1
print("❣️".utf16.count) // 2
NSRange counts UTF-16 code units (which are the “characters” in an NSString) , therefore the correct way to create an NSRange comprising the complete range of a Swift string is
let range = NSRange(location: 0, length: testStr.utf16.count)
or better (since Swift 4):
let range = NSRange(testStr.startIndex..., in: testStr)
Explanation: In your code (simplified here)
let testStr = "❣️"
let range = NSRange(location: 0, length: testStr.count)
print(range) // {0, 1}
creates an NSRange describing a single UTF-16 code unit. This cannot be converted to a Range<String.Index> in testStr because its first Character consists of two UTF-16 code units:
let wrapRange = Range(range, in: testStr)
print(wrapRange) // nil

Related

TextKit 2: Applying ParagraphStyle in custom NSTextParagraph leads to cursor position being off

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
}

Cannot spell check words of less than seven characters with UITextChecker

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.

Range position of letter in string duplicating [duplicate]

This question already has answers here:
Index of a substring in a string with Swift
(11 answers)
Closed 4 years ago.
I have a string like "abc1abc1".
What I want to do is bold each number of the string. I have drawn the code below. It works by separating each character of the string and putting them into an array. Then, in a loop, if each character contains an Int(), the character is bolded.
However, the issue comes when the there are two of the same Int. In the string above, there are 2 characters of 1, therefore the code will only bold the first character.
// Bold the numbers
let fullString = "abc1abc1"
let characters = Array(fullString)
let mutableString = NSMutableAttributedString(string: fullString)
for item in characters {
let string = String(item)
let decimalCharacters = CharacterSet.decimalDigits
let decimalRange = string.rangeOfCharacter(from: decimalCharacters)
if decimalRange != nil {
let str = NSString(string: fullString)
let range = str.range(of: string)
mutableString.addAttribute(NSAttributedStringKey.foregroundColor, value: UIFont.systemFont(ofSize: 18, weight: .heavy), range: range)
}
}
instructionsLabel.attributedText = mutableString
// characters = ["a", "b", "c", "1", "a", "b", "c", "1"]
// range of ints returns {3, 1} and {3, 1}
// range of ints SHOULD return {3, 1} and {7, 1}
Try this:
let fullString = "abc1abc1"
let range = NSRange(location: 0, length: (fullString as NSString).length)
let mutableString = NSMutableAttributedString(string: fullString)
let regex = try! NSRegularExpression(pattern: "[0-9]")
let matches = regex.matches(in: fullString, range: range)
for match in matches {
mutableString.addAttributes([.font: UIFont.systemFont(ofSize: 18, weight: .heavy)], range: match.range)
}
instructionsLabel.attributedText = mutableString

Calculate range of string from word to end of string in swift

I have an NSMutatableString:
var string: String = "Due in %# (%#) $%#.\nOverdue! Please pay now %#"
attributedText = NSMutableAttributedString(string: string, attributes: attributes)
How to calculate both the length and starting index from the word Overdue in swift?
so far I have tried:
let startIndex = attributedText.string.rangeOfString("Overdue")
let range = startIndex..<attributedText.string.finishIndex
// Access the substring.
let substring = value[range]
print(substring)
But it doesn't work.
You should generate the resulting string first:
let string = String(format: "Due in %# (%#) $%#.\nOverdue! Please pay now %#", "some date", "something", "15", "some date")
Then use .disTanceTo to get the distance between indices;
if let range = string.rangeOfString("Overdue") {
let start = string.startIndex.distanceTo(range.startIndex)
let length = range.startIndex.distanceTo(string.endIndex)
let wordToEndRange = NSRange(location: start, length: length)
// This is the range you need
attributedText.addAttribute(NSForegroundColorAttributeName,
value: UIColor.blueColor(), range: wordToEndRange)
}
Please do note that NSRange does not work correctly if the string contains Emojis or other Unicode characters so that the above solution may not work properly for that case.
Please look at the following SO answers for a better solution which cover that case as well:
https://stackoverflow.com/a/27041376/793428
https://stackoverflow.com/a/27880748/793428
Look at this,
let nsString = element.text as NSString
let range = nsString.range(of: word, options: .widthInsensitive)
let att: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.systemBlue]
attText.addAttributes(att, range: NSRange(location: range.location, length: range.length))

Checking if a word is real

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