I have a function that finds the current word a user has selected in a UITextView. However, if I call this function when an emoji is in the UITextView.text property, I see a crash. I believe this is because of the different character counts in String vs NSString.
How do I properly convert this?
func currentWord() -> String {
let cursorPosition = selectedRange.location
let separationCharacters = NSCharacterSet(charactersInString: " ")
// crash occurs here
let beginRange = Range(text.startIndex.advancedBy(0) ..< text.startIndex.advancedBy(cursorPosition))
let endRange = Range(text.startIndex.advancedBy(cursorPosition) ..< text.startIndex.advancedBy(text.characters.count))
let beginPhrase = text.substringWithRange(beginRange)
let endPhrase = text.substringWithRange(endRange)
let beginWords = beginPhrase.componentsSeparatedByCharactersInSet(separationCharacters)
let endWords = endPhrase.componentsSeparatedByCharactersInSet(separationCharacters)
return beginWords.last! + endWords.first!
}
I believe this is because of the different character counts in String vs NSString
You're right about that. You are shifting back and forth between using NSRange (Cocoa) and Range (Swift) — and they work differently. And NSString (Cocoa) and String (Swift) have different ideas of where the character boundaries are. You need to be consistent.
Once you've used selectedRange in the first line, you are in the Cocoa world of NSRange. You need to stay consistently in the Cocoa world. Don't use any Swift Ranges! Don't use any Swift characters!
Form your beginRange entirely using NSRange — for example, call NSMakeRange. Don't use characters.count; stay in the NSString world and use the string's length (in Swift, that is its utf16.count). Then all will be well.
Related
This question already has answers here:
NSRange to Range<String.Index>
(16 answers)
Closed 3 years ago.
I am trying to get a substring from a string using the range without luck. Having searched high and low, I can't find a way to do this seemingly straightforward task in Swift. The range is in the form of an NSRange obtained from a delegate method.
In Objective-c, if you have a range, you could do:
NSString * text = "Hello World";
NSString *sub = [text substringWithRange:range];
According to this answer, the following should work in Swift:
let mySubstring = text[range] // play
let myString = String(mySubstring)
However, when I try this, I get an error:
Cannot subscript a value of type 'String' with an index of type
'NSRange' (aka '_NSRange')
I think the issue may have to do with using an NSRange instead of a range but I can't figure out how to get it to work. Thanks for any suggestions.
The thing is you can not subscript a String using a NSRange, you have to use a Range. Try the following out:
let newRange = Range(range, in: text)
let mySubstring = text[newRange]
let myString = String(mySubstring)
Please read your linked question one more time.
You will notice that String in Swift doesn't work with Range<Int> but with Range<String.Index> and definitely not with NSRange
Example of using range on string:
let text = "Hello world"
let from = text.index(after: text.startIndex)
let to = text.index(from, offsetBy: 4)
text[from...to] // ello
I'm trying to create a substring of the first 4 characters entered in a textField in Swift 4 on my iOS app.
Since the change to Swift 4 I'm struggling with basic String parsing.
So based on Apple documentation I'm assuming I need to use the substring.index function and I understand the second parameter (offsetBy) is the number of characters to create a substring with. I'm just unsure how I tell Swift to start at the beginning of the string.
This is the code so far:
let postcode = textFieldPostcode.text
let newPostcode = postcode?.index(STARTATTHEBEGININGOFTHESTRING, offsetBy: 4)
I hope my explanation makes sense, happy to answer any questions on this.
Thanks,
In Swift 4 you can use
let string = "Hello World"
let first4 = string.prefix(4) // Hell
The type of the result is a new type Substring which behaves very similar to String. However if first4 is supposed to leave the current scope – for example as a return value of a function – it's recommended to create a String explicitly:
let first4 = String(string.prefix(4)) // Hell
See also SE 0163 String Revision 1
In Swift 4:
let postcode = textFieldPostcode.text!
let index = postcode.index(postcode.startIndex, offsetBy: 4)
let newPostCode = String(postcode[..<index])
I'm trying to make the words split by spaces green in a UITextField, kind of like the way it works when you compose of a new iMessage. I commented out the part of my code that's giving me a runtime error. Please let me know if you have any ideas:
func textChanged(sender : UITextField) {
var myMutableString = NSMutableAttributedString()
let arr = sender.text!.componentsSeparatedByString(" ")
var c = 0
for i in arr {
/*
myMutableString.addAttribute(NSForegroundColorAttributeName, value: UIColor.greenColor(), range: NSRange(location:c,length:i.characters.count))
sender.attributedText = myMutableString
*/
print(c,i.characters.count)
c += i.characters.count + 1
}
}
Your code has at least two parts needed to be fixed.
var myMutableString = NSMutableAttributedString()
This line creates an empty NSMutableAttributedString. Any access to the content may cause runtime error.
The other is i.characters.count. You should not use Character based locations and counts, when the APIs you want use is based on the behaviour of NSString. Use UTF-16 based count.
And one more, this is not critical, but you should use sort of meaningful names for variables.
So, all included:
func textChanged(sender: UITextField) {
let text = sender.text ?? ""
let myMutableString = NSMutableAttributedString(string: text)
let components = text.componentsSeparatedByString(" ")
var currentPosition = 0
for component in components {
myMutableString.addAttribute(NSForegroundColorAttributeName, value: UIColor.greenColor(), range: NSRange(location: currentPosition,length: component.utf16.count))
sender.attributedText = myMutableString
print(currentPosition, component.utf16.count)
currentPosition += component.utf16.count + 1
}
}
But whether this works as you expect or not depends on when this method is called.
You create an empty attributed string but never install any text into it.
The addAttribute call apples attributes to text in a string. If you try to apply attributes to a range that does not contain text, you will crash.
You need to install the content of the unattributed string into the attributed string, then apply attributes.
Note that you should probably move the line
sender.attributedText = myMutableString
Outside of your for loop. There is no good reason to install the attributed string to the text field repeatedly as you add color attributes to each word.
Note this bit from the Xcode docs on addAttribute:
Raises... an NSRangeException if any part of aRange lies beyond the
end of the receiver’s characters.
If you are getting an NSRangeException that would be a clue as to what is wrong with your current code. Pay careful attention to the error messages you get. They usually offer important clues as to what's going wrong.
I'm replacing the selected text in a textView with the new one. To accomplish this, I'm using this code based on this answer of beyowulf. All works well, the replaced text becomes selected, the problem arises when in the text there is one ore more special characters (like emoji etc). In this case the selected text misses one ore more characters at the end of the selection.
mainTextField.replaceRange((theRange), withText: newStr) // replace old text with the new one
selectNewText(theRange, newStr: newStr) // select the new text
func selectNewText(theRange: UITextRange, newStr: String) {
let newStrLength = newStr.characters.count // let's see how long is the string
mainTextField.selectedTextRange = mainTextField.textRangeFromPosition(theRange.start, toPosition: mainTextField.positionFromPosition(theRange.start, offset: newStrLength)!)
mainTextField.becomeFirstResponder()
}
OK, after I read the answers and comments to this question, I fixed this problem by replacing this statement (which returns the "human-perceptible" number of characters):
let newStrLength = newStr.characters.count
With this one:
let newStrLength = newStr.utf16.count
PS
By the way, here is some test I done with different implementations:
let str = "Abc😬"
let count = str.characters.count
print(count) // 4
let count2 = str.utf16.count
print(count2) // 5
let count3 = str.utf8.count
print(count3) // 7
I have phone number in below format
1-1xx-2xx-9565
Currently VO read it as "One (pause) One x x (pause) two x x (pause) minus nine thousand five hundred sixty five".
VO should read it as "One (pause) One x x (pause) two x x (pause) nine five six five".
What could be the problem? Is this wrong phone format?
Let's break down what is happening. VoiceOver doesn't know that the text you are presenting is a phone number and treats it like a sentence of text. In that text it tries to find distinct components and read them appropriately. For example, the text "buy 60 cantaloupes" has 3 components, "buy", "60", and "cantaloupes". The first is text and is read as text, the second is purely numerical and is best read out as "sixty", and the third is read as text.
Applying the same logic to your phone number.
(I'm not talking about actual implementation, just reasoning.)
If you read 1-1xx-2xx-9565 from the left to the right then the first distinct component is "1" which in it self is numerical and is read as "1". If the phone number would have started with "12-1xx" then the first component would have been read as "twelve" because its purely numerical.
The next component is "1xx" or "-1xx" depending on how you look at it. In either case it is a combination of numbers and letters, e.g. it is not purely numerical and is thus read out as text. If you include the "-" in that component is interpreted as a hyphen which isn't read out. That is why the the "-" is never read out for that component. The next component ("-2xx") is treated in the same way.
The final component is "-9565" which turns out to be a valid number. As seen in the cantaloupe sentence, VoiceOver reads this as a number in which case the "-" is no longer interpreted as a hyphen but as a "minus sign".
Getting VoiceOver to read your own text
On any label, view or other element in your application that is used with Voice Over, you can supply your own "accessibility label" when you know more about how you want the text to be read. This is done by simply assigning your own string to the accessibilityLabel property.
Now, you can create a suitable string in many different ways, a very simple one in your case would be to just add spaces everywhere so that each number is read individually. However, it seems a bit fragile to me, so I went ahead and used a number formatter to translate the individual numbers to their textual representations.
NSString *phoneNumber = #"1-1xx-2xx-9565";
// we want to know if a character is a number or not
NSCharacterSet *numberCharacters = [NSCharacterSet characterSetWithCharactersInString:#"0123456789"];
// we use this formatter to spell out individual numbers
NSNumberFormatter *spellOutSingleNumber = [NSNumberFormatter new];
spellOutSingleNumber.numberStyle = NSNumberFormatterSpellOutStyle;
NSMutableArray *spelledOutComonents = [NSMutableArray array];
// loop over the phone number add add the accessible variants to the array
[phoneNumber enumerateSubstringsInRange:NSMakeRange(0, phoneNumber.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
// check if it's a number
if ([substring rangeOfCharacterFromSet:numberCharacters].location != NSNotFound) {
// is a number
NSNumber *number = #([substring integerValue]);
[spelledOutComonents addObject:[spellOutSingleNumber stringFromNumber:number]];
} else {
// is not a number
[spelledOutComonents addObject:substring];
}
}];
// finally separate the components with spaces (so that the string doesn't become "ninefivesixfive".
NSString *yourAccessiblePhoneNumber = [spelledOutComonents componentsJoinedByString:#" "];
The result when I ran this was
one - one x x - two x x - nine five six five
If you need to do other modifications to your phone numbers to get them to read appropriately then you can do that. I suspect that you will use this is more than one place in your app so creating a custom NSFormatter might be a good idea.
Edit
On iOS 7 you can also use the UIAccessibilitySpeechAttributePunctuation attribute on an attributes string to change how it is pronounced.
Speech Attributes for Attributed Strings
Attributes that you can apply to text in an attributed string to modify how that text is pronounced.
UIAccessibilitySpeechAttributePunctuation
The value of this key is an NSNumber object that you should interpret as a Boolean value. When the value is YES, all punctuation in the text is spoken. You might use this for code or other text where the punctuation is relevant.
Available in iOS 7.0 and later.
Declared in UIAccessibilityConstants.h.
As of iOS 13 you can use a - NSAttributedString.Key.accessibilitySpeechSpellOut as a accessibilityAttributedLabel to make VoiceOver read each letter of the provided string (or a range of string).
So for example:
yourView.accessibilityAttributedLabel = NSAttributedString(string: yourText, attributes: [.accessibilitySpeechSpellOut: true])
If you want to spell all characters individually, a simple solution is to separate the characters by a comma ",".
You can use a String extension to convert the string:
extension String
{
/// Returns string suitable for accessibility (voice over). All characters will be spelled individually.
func stringForSpelling() -> String
{
return stringBySeparatingCharactersWithString(",")
}
/// Inserts a separator between all characters
func stringBySeparatingCharactersWithString(separator: String) -> String
{
var s = ""
// Separate all characters
let chars = self.characters.map({ String($0) })
// Append all characters one by one
for char in chars {
// If there is already a character, append separator before appending next character
if s.characters.count > 0 {
s += separator
}
// Append next character
s += char
}
return s
}
}
And then use it in the code like this:
myLabel.accessibilityLabel = myString.stringForSpelling()
Just Add a comma to each digit of the last number and after the last digit as well,. this will make sure voice over reads the last number as same as previous numbers.
example your number :- 1-1xx-2xx-9565
accessibility label :- 1-1xx-2xx-9,5,6,5,
Here is the code in Swift
public func retrieveAccessiblePhoneNumber(phoneNumber: String) -> String {
// We want to know if a character is a number or not
let characterSet = NSCharacterSet(charactersInString: "0123456789")
// We use this formatter to spell out individual numbers
let numberFormatter = NSNumberFormatter()
numberFormatter.numberStyle = .SpellOutStyle
var spelledOutComponents = [String]()
let range = Range<String.Index>(start: phoneNumber.startIndex, end: phoneNumber.endIndex)
// Loop over the phone number add add the accessible variants to the array
phoneNumber.enumerateSubstringsInRange(range,
options: NSStringEnumerationOptions.ByComposedCharacterSequences) { (substring, substringRange, enclosingRange, stop) -> () in
// Check if it's a number
if let substr = substring where substr.rangeOfCharacterFromSet(characterSet) != nil {
if let number = Int(substr) {
// Is a number
let nsNumber = NSNumber(integer: number)
spelledOutComponents.append(numberFormatter.stringFromNumber(nsNumber)!)
}
} else {
// Is not a number
spelledOutComponents.append(substring!)
}
}
// Finally separate the components with spaces (so that the string doesn't become "ninefivesixfive".
return spelledOutComponents.joinWithSeparator(" ")
}