Not able to get deleted character from UITextView in iOS - ios

I am trying to retrieve the deleted character from UITextView when user presses delete button on keypad.
When I enter some text in textview for eg:(abc), I am able to retrieve the deleted character by checking the range and location. So no problem for english text
The challenge starts when I have a non english text, for eg "Hindi" language.
Lets say i entered "परक" in textview, and then if I delete one character at a time from right to left, I am able to identify the deleted character as क, र, प respectively.
The real problem start when I have Hindi vowels in the string
For eg: If i have a text as "परी". Then according to Hindi text it contains three characters as प, र, ी. Here ी is a long vowel in Hindi language
When i enter ी in textview, the delegate method is able to identify this text.
However when i press delete button on keyboard to delete ी from word "परी", the range calculation to determine which word is deleted seems to go wrong.
I get range value to delete ी as
Range(3..<3)
- startIndex : 3
- endIndex : 3
Since startIndex and endIndex are same, I get an empty string.
Following is the code I am using to enter or delete a character in textview
func textView(textView: UITextView, shouldChangeTextInRange range: NSRange, replacementText text: String) -> Bool {
if ( range.location == 0 && text == " " )
{
return false
}
// Its backspace
if text == ""
{
// Set startIndex for Range
let startIndex = textView.text.startIndex.advancedBy(range.location, limit: textView.text.endIndex)
// Set new Range
let newRange = startIndex..<textView.text.startIndex.advancedBy(range.location + range.length, limit: textView.text.endIndex)
// Get substring
// Here I get the character that is about to be deleted
let substring = textView.text[newRange]
print(substring) //Gives empty string when i delete ी
return true
}
print(text) // This prints ी when i enter it in textview after पर
return true
}
It would be great if anyone can help me with this. I have checked with Android and it is able to identify ी, when deleted.
Awaiting for replies curiously....
Note: Code is written in Swift 2.3

When I removed the last character of "परी" in a UITextView, I got NSRange(location: 2, length: 1) in the delegate method. It's not empty, as you see, its length is not 0.
The problem is that your code of retrieving the substring is completely wrong.
When an iOS API returns an NSRange for string related operations, its location and length are based on UTF-16, which is the basis of NSString. You should not use those values for characters (String.CharacterView) based index calculation.
Replace these lines of your code:
// Set startIndex for Range
let startIndex = textView.text.startIndex.advancedBy(range.location, limit: textView.text.endIndex)
// Set new Range
let newRange = startIndex..<textView.text.startIndex.advancedBy(range.location + range.length, limit: textView.text.endIndex)
// Get substring
// Here I get the character that is about to be deleted
let substring = textView.text[newRange]
To:
//### Get substring
let substring = (textView.text as NSString).substringWithRange(range)
Your code seems to be written in Swift 2, but in Swift 3, it becomes:
//### Get substring
let substring = (textView.text as NSString).substring(with: range)

Related

Changing font of strings separated by spaces

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.

Swift - How can I force users to import only integer in UITextField?

Normally, in Text Field, users can enter a String, and even if the users entered a number, the program would automatically understand it as a string.
So, here is my problem. I want to make a program evaluating the speed of a motorcyclist.
I have a text field, a text view and a button START. What I want to do is to apply SWITCH - CASE in classifying the number that the users enter in the text field, and then I will print out my evaluations to the text view, such as "Slow", "Fast Enough" or "Dangerously Fast".
However before applying switch - case, I think that I have to force the users to only enter Integer numbers to the text field. And if they enter any alphabet letter, the text view will appear: "Wrong format! Please try again!"
I think that I have to do something with the statement if to solve the problem, but the truth is I've just started learning Swift, and couldnt think of any possible solutions. Please help me.
Thank you.
If you are using storyboard just select the TextField and change the Keyboard type to NumberPad. This will only allow integers to be entered. Then you could just turn it into a Int when you get back the input.
if let convertedSpeed = Int(textField.text) {
// Implement whatever you want
} else {
// Notify user of incorrect input
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
if textField == Number_Txt // your text filed name
{
var result = true
let prospectiveText = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
if string.characters.count > 0
{
let disallowedCharacterSet = NSCharacterSet(charactersInString: "0123456789").invertedSet
let replacementStringIsLegal = string.rangeOfCharacterFromSet(disallowedCharacterSet) == nil
let resultingStringLengthIsLegal = prospectiveText.characters.count > 0
let scanner = NSScanner(string: prospectiveText)
let resultingTextIsNumeric = scanner.scanDecimal(nil) && scanner.atEnd
result = replacementStringIsLegal && resultingStringLengthIsLegal && resultingTextIsNumeric
}
return result
}
else
{
return true
}
}
You can solve it in two ways.
Convert the typed text to Integer value.
Int(textfield.text!)
This one is very simpler. Choose the keyboard type as Numeric/ Numbers and Punctuation pad. So that, user can type only the nos.
Hope it helps..
You can specify the keyboard type of a textfield in storyboard, under attributes inspector.
"Decimal" would be the way to go for you (assuming that possible input can be e.g. 180.5)
To move on, you still can check the input like this:
if (Int(textfield.text!) != nil ) {
//Valid number, do stuff
} else {
textfield.text = "Wrong format! Please try again!"
}
EDIT:
The ' != nil ' means the following:
The Initializer of Int is failable. That means if you pass a string which does not contain a valid number, it will return nil (null if you are coming from java/c#). But if the string does contain a valid number, it will return a valid Int, therefore its not nil. I hope this makes it clear to you ;)

Swift "can not increment endIndex" when deleting Japanese characters

I'm using Swift 2.0/Xcode 7.0 for an iOS app. I've built a Japanese IME to convert Roman characters to their Japanese equivalent. For example:
Typing "a" will convert the "a" to "あ"
Typing a "k" will do nothing (no match in Japanese) until the next character is typed, for example typing a "k" then an "a" will result in "か"
My problem is when I try to delete a Japanese character. If there is only one character in the text field, the delete key/function works as expected. However, if there is more than one, when I try to delete the character, I receive a can not increment endIndex error in the following code.
var imeInputLength: Int = 0
let currentInputValue: String = txtfldYourResponse.text!.lowercaseString
if(currentInputValue.characters.count==0) {
imeInputLength = 0
}
let inputStringToKeep: String = currentInputValue.substringWithRange(
Range<String.Index>(start: currentInputValue.startIndex.advancedBy(imeInputLength),
end: currentInputValue.endIndex))
let imeStringToKeep: String = currentInputValue.substringWithRange(
Range<String.Index>(start: currentInputValue.startIndex,
end: currentInputValue.startIndex.advancedBy(imeInputLength)))
if let imeValueDC = JIMEDC[inputStringToKeep] {
txtfldYourResponse.text = imeStringToKeep + imeValueDC
imeInputLength = (txtfldYourResponse.text?.characters.count)!-1
}
if let imeValue = JIME[inputStringToKeep] {
txtfldYourResponse.text = imeStringToKeep + imeValue
imeInputLength = txtfldYourResponse.text!.characters.count
}
currentInputValue is the text from the text field.
imeInputLength is an int (initial value = 0) that increments by the total character count in the text field AFTER a match is found.
JIMEDC and JIME are key/value pairs that handle the conversion from Roman to Japanese characters.
I'm printing the endIndex to the console before the code runs. It appears to increment/decrement as expected, but the code block above fails with the increment error.
I've been banging my head against this for a couple weeks now with no progress.
Edit: Clarified handling of imeInputLength and added additional code for clarification.
A side conversation with l'L'l got me on the right track. I was confused by the error message can not increment endIndex. In reality, I was creating an out-of-bounds error on my range.
When the backspace key was pressed, I wasn't modifying imeInputLength to reflect the new length of the input string. As a result, my advanceByamount was larger than the updated endIndex value. Swift threw this as can not increment endIndex but the reason for it was the startIndex was now greater than the endIndex.
I resolved this by first checking for the following:
if(imeInputLength > currentInputValue.characters.count) {
print("You must have pressed backspace")
imeInputLength = txtfldYourResponse.text!.characters.count
} else {
let inputStringToKeep:
...
}
If the backspace key IS pressed, I needed to see if the imeInputLength value was greater than the current character count of the input field. The only use case where imeInputLength ends up greater is when a Japanese character is deleted using the backspace key. When this condition is caught, I reset imeInputLength to the current character count and exit the routine.
I'm happy to say that all of my use cases now test out positive!

How to delete whole blocks of NSAttributedString

I am working on an app that lets the user mention other users by typing #username. Though I have found a way to detect the # character and highlight the selected username within the text string, I am not sure how to manage the event when the user mentions a user but then wants to delete that user from the mention.
For example, in the Facebook app, when you mention someone, that person's name in the UITextView is highlighted with a light-blue background. However, when you start deleting that mention, the whole mention gets deleted when you delete the last character in the attributed string.
I am, therefore, looking for a way that I can capture when the user deletes the last character of an attributed string in order to delete that whole attributed string and completely remove the mention from the text view.
In order to achieve the desired behaviour mentioned above, I ended up doing the following. I implemented all these methods as part of the view controller where my UITextView is placed, so I added the UITextViewDelegate protocol to my view controller. Then, I implemented the following methods:
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text{
//if this is true, then the user just deleted a character by using backspace
if(range.length == 1 && text.length == 0){
NSUInteger cursorPosition = range.location; //gets cursor current position in the text
NSRange attrRange; //will store the range of the text that holds specific attributes
NSDictionary *attrs = [_content.attributedText attributesAtIndex:cursorPosition effectiveRange:&attrRange];
//check if the attributes of the attributed text in the cursor's current position correspond to what you want to delete as a block
if([attrs objectForKey:NSBackgroundColorAttributeName]){
NSAttributedString *newStr = [_content.attributedText attributedSubstringFromRange:NSMakeRange(0, attrRange.location)]; //creates a new NSAttributed string without the block of text you wanted to delete
_content.attributedText = newStr; //substitute the attributed text of your UITextView
return NO;
}
}
return YES;
}
Luis Delgado's excellent answer gave me a great jumping off point, but has a few shortcomings:
always clears to the end of the string
does not handle insertion into the token
does not handle modifying selected text that includes token(s)
Here is my version (Swift 4):
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if range.location < textView.attributedText.length {
var shouldReplace = false
var tokenAttrRange = NSRange()
var currentReplacementRange = range
let tokenAttr = NSAttributedStringKey.foregroundColor
if range.length == 0 {
if nil != textView.attributedText.attribute(tokenAttr, at: range.location, effectiveRange: &tokenAttrRange) {
currentReplacementRange = NSUnionRange(currentReplacementRange, tokenAttrRange)
shouldReplace = true
}
} else {
// search the range for any instances of the desired text attribute
textView.attributedText.enumerateAttribute(tokenAttr, in: range, options: .longestEffectiveRangeNotRequired, using: { (value, attrRange, stop) in
// get the attribute's full range and merge it with the original
if nil != textView.attributedText.attribute(tokenAttr, at: attrRange.location, effectiveRange: &tokenAttrRange) {
currentReplacementRange = NSUnionRange(currentReplacementRange, tokenAttrRange)
shouldReplace = true
}
})
}
if shouldReplace {
// remove the token attr, and then replace the characters with the input str (which can be empty on a backspace)
let mutableAttributedText = textView.attributedText.mutableCopy() as! NSMutableAttributedString
mutableAttributedText.removeAttribute(tokenAttr, range: currentReplacementRange)
mutableAttributedText.replaceCharacters(in: currentReplacementRange, with: text)
textView.attributedText = mutableAttributedText
// set the cursor position to the end of the edited location
if let cursorPosition = textView.position(from: textView.beginningOfDocument, offset: currentReplacementRange.location + text.lengthOfBytes(using: .utf8)) {
textView.selectedTextRange = textView.textRange(from: cursorPosition, to: cursorPosition)
}
return false
}
}
return true
}
If you need to override a superclass, add the override keyword and instead of returning true at the end, return
super.textView(textView, shouldChangeTextIn: range, replacementText: text)
You can change tokenAttr to any standard or custom text attribute. If you change enumerateAttribute to enumerateAttributes, you could even look for a combination of text attributes.
If you already have the entire #username surrounded with an attribute, subclass UITextView and override -deleteBackward to check if the cursor is inside one of the username attributes. If not, just call the super implementation, otherwise find the starting location and the length of the attributed username and delete the entire range.

Swift - Maintaining decimal point with Editing Changed

I am creating a unit converter and want all the units to update simultaneously as the user enters a value. e.g. entering 25 into the 'cm' field will automatically display 250 in the 'mm' field and 0.25 in the 'metre' field.
This is so far working great using Editing Changed event so long as the user enters whole numbers, as soon as a decimal is entered iOS automatically strips the decimal away to format the UITextField as a whole number since it is formatting after each keypress and assumes the the number entered ends with a decimal point i.e "25." which then becomes "25" instead of "25.0".
For instance entering 25.6 will display as 256.
Is there any way to prevent this automatic formatting? The only solution I have found is to link the event to Editing Did End but this is not ideal as the other units will only become updated after the user finishes entering the value and not updated automatically as I require.
The code I'm using to convert from string to double is as follows:
#IBAction func editMm(sender: AnyObject) {
var setToDouble:Double? = stringToDouble(textFieldMm.text)
valueEdited(setToBase!)
}
func stringToDouble(inputString:String) -> Double{
// this removes the comma separators which are automatically added to the UITextField during formatting
var formattedString = inputString.stringByReplacingOccurrencesOfString(",", withString: "")
var doubleResult:Double = (formattedString as NSString).doubleValue
return doubleResult
}
I've tried to catch this in the stringToDouble function but it seems the formatting happens before the value reaches that point in the code.
You can use the shouldChangeCharactersInRange function in UITextFieldDelegate. The newString will contain the current string while editing.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let newString = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
println(newString)
return true
}

Resources