How to delete whole blocks of NSAttributedString - ios

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.

Related

How can I disable the return key in a UITextView based on the number of empty rows in the UITextView?

Both TikTok & Instagram (iOS) have a mechanism built into their edit profile biography code which enables the user to use the return key and create separation lines in user's profile biographies. However, after a certain number of lines returned with no text in the lines, they prevent the user from using the return key again.
How can one do this?
I understand how to prevent the return key from being used if the present line the cursor is on is empty, using the following:
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
guard text.rangeOfCharacter(from: CharacterSet.newlines) == nil else {
return false
}
return true
Moreover, I need help figuring out how to detect, for example, that 4 lines are empty, and stating that if 4 lines are empty, preventing the user from using the return key.
This may need fine tuning for edge cases but this would be my starting point for sure. The idea is to check the last 4 inputs into the text view with the current input and decide what to do with it. This particular code would prevent the user from creating a fourth consecutive empty line.
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if text == "\n",
textView.text.hasSuffix("\n\n\n\n") {
return false
}
return true
}
I am not completely sure I have understood your question correctly. But let me try to help.
A UITextView has a text property that you can read. Then, if a user enters a new character/inserts new text (make sure to test this with copy-pasting text), you could check whether the last three characters of the text are newlines or not. If so, and the user is trying to add another newline, you know to return false.
This would look like this:
let lastThreeCharacters = textView.text.suffix(3)
let lastThreeAreNewlines = (lastThreeCharacters.count == 3) && lastThreeCharacters.allSatisfy( {$0.isNewline} ) // Returns true if the last 3 characters are newlines
You'd need to implement some additional checks. Is the character that's going to be inserted a newline? If the user pastes text, will the last 4 characters be newlines?
Another method would be to make use of another method of the UITextViewDelegate. You could also implement textViewDidChange(_:), which is called after a user has changed the text. Then, check if (and where) the text contains four new lines and replace them with empty characters.
This would look something like this (taken from here):
func textViewDidChange(_ textView: UITextView) {
// Avoid new lines also at the beginning
textView.text = textView.text.replacingOccurrences(of: "^\n", with: "", options: .regularExpression)
// Avoids 4 or more new lines after some text
textView.text = textView.text.replacingOccurrences(of: "\n{4,}", with: "\n\n\n", options: .regularExpression)
}

iOS 11 disable password autofill accessory view option?

As of now I would like to opt out of the new option iOS 11 gives, that is to suggest passwords in the app. When I run the app on iOS 11 I get the autofill option on top of the keyboard and my username and password textfield don't even show up.
So, my question is, how can I disable the new password autofill feature all together so the key on the keyboard is not shown at all and the overall behavior is the same as pre iOS 11?
iOS 11 & 12 & 13 - Swift 4.2 & 5 (Updated):
if #available(iOS 12, *) {
// iOS 12 & 13: Not the best solution, but it works.
passwordTextField.textContentType = .oneTimeCode
} else {
// iOS 11: Disables the autofill accessory view.
// For more information see the explanation below.
emailTextField.textContentType = .init(rawValue: "")
passwordTextField.textContentType = .init(rawValue: "")
}
iOS 11 explanation:
Make sure you setup all of your UITextField objects like this.
If you have for example an UITextField object where the user must enter his email address and another one where the user must enter his password assign UITextContentType("") to both of their textContentType property. Otherwise it will not work and the autoFill accessory view will still be shown.
iOS 12 seems to recognise password textFields also by isSecureTextEntry property and not just by textContentType property, so making this accessory view disappear is not really possible unless you both set textContentType to nothing, and remove the secureEntry feature (and cause a security flaw in your app) which then prevents iOS 12 to recognise the textField as a password textField and show this annoying accessory view.
In my case the accessory caused a bug which made my app unresponsive when tapped (Which also got my app rejected in app review process). So I had to remove this feature. I didn't want to give on up this security feature so I had to solve things by my self.
The idea is to remove the secureEntry feature but add it by yourself manually. It did worked:
It can be done like that:
Swift 4 way:
First, as answered here, set textContentType to nothing:
if #available(iOS 10.0, *) {
passwordText.textContentType = UITextContentType("")
emailText.textContentType = UITextContentType("")
}
Than, declare a String variable which will later contain our textField real content:
var passwordValue = ""
Add a target to the passwordTextField, which will be called each time the textField content changes:
passwordText.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
Now That's what will do the magic, declare the function that will handle the text replacements:
#objc func textFieldDidChange(_ textField: UITextField) {
if textField.text!.count > 1 {
// User did copy & paste
if passwordValue.count == 0 { // Pasted into an empty textField
passwordValue = String(textField.text!)
} else { // Pasted to a non empty textField
passwordValue += textField.text!.substring(from: passwordValue.count)
}
} else {
// User did input by keypad
if textField.text!.count > passwordValue.count { // Added chars
passwordValue += String(textField.text!.last!)
} else if textField.text!.count < passwordValue.count { // Removed chars
passwordValue = String(passwordValue.dropLast())
}
}
self.passwordText.text = String(repeating: "•", count: self.passwordText.text!.count)
}
Finally, Set textField's autocorrectionType to .no to remove predictive text:
passwordText.autocorrectionType = .no
That's it, use passwordValue to perform your login.
Hope it'll help someone.
UPDATE
It catches pasted values also, forgot to add it before.
The feature can be disabled by specifying a content type that is neither username nor password. For example, if the user should enter an email address, you could use
usernameTextField?.textContentType = .emailAddress
in response to #Gal Shahar Answer.
iOS 12 recognise password textFields by isSecureTextEntry property and not just by textContentType property.
Way Around to bypass Auto-fill Suggestion.
set isSecureTextEntry property to false.
self.passwordTextField.secureTextEntry = NO;
Add a UITextField Delegate Method and enable the isSecureTextEntry property.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
if (textField == self.passwordTextField && !self.passwordTextField.secureTextEntry) {
self.passwordTextField.secureTextEntry = YES;
}
return YES;
}
NOTE: -
Do NOT Use shouldBeginEditing UITextField delegate method it Will Still show Auto-filling Suggestion.
Do NOT Use textFieldDidChange UITextField delegate method it Will Auto-delete the first charachter as the it Will happen after the first charachter is displayed. And 'secureTextEntry' will empty the field.
You can add extension for UITextContentType like this
extension UITextContentType {
public static let unspecified = UITextContentType("unspecified")
}
after that, you can use it
if #available(iOS 10.0, *) {
passwordField.textContentType = .unspecified
}
A very simple approach in ios11 worked for me. Suppose your iboutlets are usernametextfield and passwordtextfield. In viewDidLoad() function of your viewcontroller that holds the both outlest use the following code
usernametextfield.textContentType = UITextContentType("")
passwordtextfield.textContentType = UITextContentType("")
After this you wont see autofill accessory option when you tap on your textfields.
Objective-C
if (#available(iOS 10, *)){
self.tfEmail.textContentType = #"";
self.tfPassword.textContentType = #"";
}
This worked for me.
In my case the solution with setting textContentType = .oneTimeCode for password fields worked only partially. On the device with iOS 15 I did not saw a problem but on the simulator with iOS 15 it did not work properly especially when switching between the fields. Also it's worth saying that I had a button for showing/hiding passwords in fields which toggle the value in isSecureTextEntry. However after setting textContentType = .oneTimeCode on all fields on the screen (not only password fields but also other name and email fields which I had) then it started to work properly also on the simulator. So if the .oneTimeCode solution does not work for you then you might want to try setting it on all fields on your screen not only password fields.
Autofill is by default enabled for users. iOS saves all passwords in the keychain and make them available in keyboard in your apps. UITextView and UITextField automatically considered for AutoFill password. you can disable by specifying a content type that is neither username nor password but if the content type info already stored in keychain it will show in the quick bar. so better to assign empty UITextContentType type and it will not show quickbar.
Example:
if #available(iOS 10.0, *) {
self.textField.textContentType = UITextContentType("")
} else {
// Fallback on earlier versions
}
This worked for ios 12 and 10:
if (#available(iOS 10, *)) {
passwordTextField.textContentType = UITextContentTypeStreetAddressLine2;
}
if (#available(iOS 12, *)) {
passwordTextField.textContentType = UITextContentTypeOneTimeCode;
}
Crazy staff happening in this topic. I've made without iOS proposing me a password, but given me an autofill for email only. Just if someone need it so. After different combinations and different type of textContentType I've made it as I wanted.
Email Text field with iOS autofill proposing be something.
Password text field with no iOS system proposing me a new password, but filling it with password if I choose it from email text field.
Password text field to have secure entry text.
And with this code it worked. Doesn't matter if you have email or username it will make you a proposition with what you need. So I've disabled accessory autofill view and left only autofill in toolbar of keyboard.
self.passwordField.isSecureTextEntry = true
if #available(iOS 11.0, *) {
self.emailField.textContentType = .username
self.emailField.keyboardType = .emailAddress
}
if #available(iOS 12.0, *) {
self.passwordField.textContentType = .password
self.passwordField.keyboardType = .default
}
Objective C, iOS 15:
textField.textContentType = UITextContentTypeOneTimeCode;
textField.autocorrectionType = UITextAutocorrectionTypeNo;
On the simulator, it still may show suggestions, on the device doesn't.
You can "turn off" the username/password combo detection by assigning a dummy textContentType to the password text field.
passwordFormField.textContentType = UITextContentType("dummy")
This turned off the key symbol for both the password field and the email field that preceded it, and in this way you don't use one of the predefined values and you avoid showing unrelated suggestions in the keyboard accessory view.
You could try different answers here that conclude it's likely possible to remove the accessory view. But this leaves some bugs.
You could try to implement a custom keyboard perhaps, only for password fields. Also try to disable suggestions for your textfield, I think that also hides the accessoryView.
EDIT:
Still no answer on Apple forums on the same question. Also I couldn't find anything regarding this in official UITextField documentation.
Ok, I can confirm that I have the following working on iOS 14 with no issues:
// once this property is declared, textField.keyboardType does
// not seem to have any effect in showing the autofill
textField.textContentType = UITextContentType.oneTimeCode
textField.keyboardType = UIKeyboardType.emailAddress
From what I can tell, Bem's answer does not work in iOS 12 and Gal Shahar's answer does not account for some edge cases (for example, if a user deletes multiple characters at once). I worked around this using an IBAction, thus removing the necessity to check for iOS version altogether. I'm just a beginner, so this might not be the "best" answer or the most efficient, but it made the most sense to me:
First, uncheck "Secure Text Entry" in the Storyboard or set it to "false"/"NO" via code for your password UITextField. This will prevent iOS from attempting to AutoFill.
Then, link your password UITextField to an IBAction. Mine is called on:
Editing did begin
Editing did change
Editing did end
The IBAction function I wrote determines the differences between a user's starting password and what has been inputed into the password UITextField and creates a new password based on this information:
class Login: UIViewController {
var password = ""
override func viewDidLoad() { super.viewDidLoad() }
#IBAction func editPasswordField(_ sender: UITextField) {
var input = Array(sender.text ?? "")
var oldPassword = Array(password)
var newPassword = Array("")
//if character(s) are simply deleted from "passwordField" (not replaced or added to), "cursorPosition" is used to determine which corresponding character(s) need to also be removed from "oldPassword"
//this is indicated by "input" comprising of only "•" (bullets) and being shorter in length than "oldPassword"
var onlyBullets = true
for char in input { if char != "•" { onlyBullets = false } }
if onlyBullets && input.count < oldPassword.count {
if let selectedRange = sender.selectedTextRange {
let cursorPosition = sender.offset(from: sender.beginningOfDocument, to: selectedRange.start)
let prefix = String(oldPassword.prefix(cursorPosition))
let suffix = String(oldPassword.suffix(input.count - cursorPosition))
input = Array(prefix + suffix)
} else { input = Array("") }
}
//if no changes were made via input, input would comprise solely of a number of bullets equal to the length of "oldPassword"
//therefore, the number of changes made to "oldPassword" via "input" can be measured with "bulletDifference" by calculating the number of characters in "input" that are NOT bullets
var bulletDifference = oldPassword.count
for char in input { if char == "•" { bulletDifference -= 1 } }
//the only way "bulletDifference" can be less than 0 is if a user copy-pasted a bullet into "input", which cannot be allowed because it breaks this function
//if a user pastes bullet(s) into "input", "input" is deleted
//an edge case not accounted for is pasting a mix of characters and bullets (i.e. "ex•mple") when "oldPassword.count" exceeds the number of bullets in the mixed input, but this does not cause crashes and therefore is not worth preventing
if bulletDifference < 0 {
bulletDifference = oldPassword.count
input = Array("")
}
//"bulletDifference" is used to remove every character from "oldPassword" that corresponds with a character in "input" that has been changed
//a changed character in "input" is indicated by the fact that it is not a bullet
//once "bulletDifference" equals the number of bullets deleted, this loop ends
var bulletsDeleted = 0
for i in 0..<input.count {
if bulletsDeleted == bulletDifference { break }
if input[i] != "•" {
oldPassword.remove(at: i - bulletsDeleted)
bulletsDeleted += 1
}
}
//what remains of "oldPassword" is used to substitute bullets in "input" for appropriate characters to create "newPassword"
//for example, if "oldPassword" is "AcbDE" and "input" is "•bc••", then "oldPassword" will get truncated to "ADE" and "newPassword" will equal "A" + "bc" + "DE", or "AbcDE"
var i = 0
for char in input {
if char == "•" {
newPassword.append(oldPassword[i])
i += 1
} else { newPassword.append(char) }
}
password = String(newPassword)
//"passwordField.text" is then converted into a string of bullets equal to the length of the new password to ensure password security in the UI
sender.text = String(repeating: "•", count: password.count)
}
}
Constructive criticism is appreciated!
I have attached the screenshot. you can change the content Type as Username in the storyboard or you can do programatically.
you may create an extension for UITextfield .
func diableAutofill() {
self.autocorrectionType = .no
if #available(iOS 11.0, *) {
self.textContentType = .username
} else {
self.textContentType = .init("")
}
}
change textfield content type to
Obj C
if (#available(iOS 12.0, *)) {
self.curresntPasswordTxField.textContentType = UITextContentTypeOneTimeCode;
} else {
// Fallback on earlier versions
}
Swift
self.curresntPasswordTxField.textContentType = .oneTimeCode
self.passwordTextField.autocorrectionType = NO;
It does not seem to work, the keychain sign still there,
self.passwordTextField.textContentType = UITextContentTypeName;
The code above does work, but if the users set up their Apple ID account, Then, the name of the apple id will be display on the keyboard you cannot disable it by set autocorrectionType to No, not sure if Apple still refines this autofill feature, it's quite buggy right now.
This worked for me:
NOTE: try putting this code on the password, password confirm (if applicable), AND email textfields. I was not putting it on the email textfield and it was still popping up for the two password fields.
if #available(iOS 12, *) {
// iOS 12: Not the best solution, but it works.
cell.textField.textContentType = .oneTimeCode
} else {
// iOS 11: Disables the autofill accessory view.
cell.textField.textContentType = .init(rawValue: "")
}
I think set all UITextField textContentType in form to UITextContentType("") or .oneTimeCode is not a clean solution. Enable/Disable isSecureTextEntry still give you the same issue.
#Gal Shahar 's Answer is nice but it is still not perfect. The masked char is not the same as the masked char that used in secure entry text from apple. It should use Unicode Character 'BLACK CIRCLE' (U+25CF) https://www.fileformat.info/info/unicode/char/25cf/index.htm
Also, it is not handling cursor movement. It will change the cursor position to the end of the text when inserting text in the middle. It will give you the wrong value when selecting and replacing text.
When you decide to use custom isSecureEntryText to avoid autofill password, here is the code:
Swift 5 (simple version)
#IBOutlet weak var passwordTextField: UITextField!
var maskedPasswordChar: String = "●"
var passwordText: String = ""
var isSecureTextEntry: Bool = true {
didSet {
let selectedTextRange = passwordTextField.selectedTextRange
passwordTextField.text = isSecureTextEntry ? String(repeating: maskedPasswordChar, count: passwordText.count) : passwordText
passwordTextField.selectedTextRange = selectedTextRange
}
}
//this is UITextFieldDelegate
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == passwordTextField {
//update password string
if let swiftRange = Range(range, in: passwordText) {
passwordText = passwordText.replacingCharacters(in: swiftRange, with: string)
} else {
passwordText = string
}
//replace textField text with masked password char
textField.text = isSecureTextEntry ? String(repeating: maskedPasswordChar, count: passwordText.count) : passwordText
//handle cursor movement
if let newPosition = textField.position(from: textField.beginningOfDocument, offset: range.location + string.utf16.count) {
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
return false
}
return true
}
Swift 5 (COMPLETE version with securing last char animation)
private struct Constants {
static let SecuringLastCharPasswordDelay = 1.5
}
#IBOutlet weak var passwordTextField: UITextField!
private var secureTextAnimationQueue: [String] = []
var maskedPasswordChar: String = "●"
var passwordText: String = ""
var isSecureTextEntry: Bool = true {
didSet {
secureTextAnimationQueue.removeAll()
let selectedTextRange = passwordTextField.selectedTextRange
passwordTextField.text = isSecureTextEntry ? String(repeating: maskedPasswordChar, count: passwordText.count) : passwordText
passwordTextField.selectedTextRange = selectedTextRange
}
}
//this is UITextFieldDelegate
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == passwordTextField {
//update password string
if let swiftRange = Range(range, in: passwordText) {
passwordText = passwordText.replacingCharacters(in: swiftRange, with: string)
} else {
passwordText = string
}
//replace textField text with masked password char
updateTextFieldString(textField, shouldChangeCharactersIn: range, replacementString: string)
//handle cursor movement
if let newPosition = textField.position(from: textField.beginningOfDocument, offset: range.location + string.utf16.count) {
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}
return false
}
return true
}
private func updateTextFieldString(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) {
if isSecureTextEntry {
if string.count == .one, let text = textField.text {
let maskedText = String(repeating: maskedPasswordChar, count: text.count)
var newMaskedText = String()
if let swiftRange = Range(range, in: maskedText) {
newMaskedText = maskedText.replacingCharacters(in: swiftRange, with: string)
} else {
newMaskedText = text + maskedText
}
textField.text = newMaskedText
secureTextAnimationQueue.append(string)
asyncWorker.asyncAfter(deadline: .now() + Constants.SecuringLastCharPasswordDelay) { [weak self] in
self?.securingLastPasswordChar()
}
} else {
secureTextAnimationQueue.removeAll()
textField.text = String(repeating: maskedPasswordChar, count: passwordText.count)
}
} else {
textField.text = passwordText
}
}
private func securingLastPasswordChar() {
guard secureTextAnimationQueue.count > .zero, isSecureTextEntry else { return }
secureTextAnimationQueue.removeFirst()
if secureTextAnimationQueue.count == .zero {
let selectedTextRange = passwordTextField.selectedTextRange
passwordTextField.text = String(repeating: maskedPasswordChar, count: passwordText.count)
passwordTextField.selectedTextRange = selectedTextRange
}
}
Thanks to Apple, I couldn't find any way with native methods when isSecureTextEntry setted to YES. Gal Shahar's way is the only solution for disabling password autofill accessory view option. But It's more easy to do with
objective c
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
swift
textField(_:shouldChangeCharactersIn:replacementString:)
delegate. And use simple code like this. Declare a String variable which will later contain our textField real content, mine is pswd.
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
//Thanks iOS13!!!
if(!_pswd)
{
_pswd = #"";
}
_pswd = [_pswd stringByReplacingCharactersInRange:range withString:string];
if (!buttonShowPassword.selected)
{
textField.text = [#"" stringByPaddingToLength:_pswd.length withString: #"•" startingAtIndex:0];
}
else
{
textField.text = _pswd;
}
return NO;
}
for SwiftUI do as follow:
SecureField("Password", text: "some text").disableAutocorrection(true)

Not able to get deleted character from UITextView in 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)

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 ;)

Can I set the insertion point on a UITextField

I have a UITextField that I want to present to the user with a pre-filled suffix. Obviously, I'd like to have the insertion point set to the beginning of the pre-filled text.
It doesn't seem obvious to me how to do this (e.g., there's no insertionPoint property), but is there a tricky way to get this done?
Override drawTextInRect: to always draw your suffix, even though it is not contained in the UITextFields text (use a temp string that takes the textfield.text, appends your suffix, and draws it). Once the user is done editing, then append your suffix to the string (or use them separately if it helps).
Everybody loves a 9 year old question.
UITextField conforms to UITextInput. So all the methods you're looking for in the class documentation are nowhere to be found.
In my example, A string of "$1234.00" was to be displayed, but only a 1234 range of the string could be edited.
textField.delegate = self
textField.text = "$\(myIntValue).00"
The textFieldDidBeginEditing method selects all of the editable area, another valid/useful selection would be between the 1234 and the ..
Note that we are working with both UITextRange an NSRange. They aren't exactly interchangeable.
extension MyViewController : UITextFieldDelegate {
/// Set the "insertion caret" for the text field.
func textFieldDidBeginEditing(_ textField: UITextField) {
if let startPosition = textField.position(from: textField.beginningOfDocument, offset: 1), // forward over "$"
let endPosition = textField.position(from: textField.endOfDocument, offset: -3) { // back 3 over ".00"
let selection = textField.textRange(from: startPosition, to: endPosition)
//let selection = textField.textRange(from: endPosition, to: endPosition)
textField.selectedTextRange = selection
}
}
/// Only allow edits that are in the editable range and only adding digits or deleting anything (this allows cut/paste as well)
func textField(_ textField:UITextField, shouldChangeCharactersIn range:NSRange, replacementString string:String) -> Bool {
let minimalString = "$.00" // String always starts in uneditable "$", and ends in an uneditable ".00"
assert(textField.text?.count ?? 0 >= minimalString.count)
guard let text = textField.text else { return false }
if range.upperBound > text.count - 3 || range.lowerBound == 0 { return false } // range only in editable area
if string.components(separatedBy: CharacterSet.decimalDigits.inverted).joined().count != string.count { return false } // new string only has digits
return true
}
}
As far as I know, there's no simple way to do this.
I'd say that the simplest way to do this is to add the suffix after the user entered his text. Don't forget to add something in your UI to tell the user there will be something added.
You could have the suffix as a UILabel right next to the end of the UITextField, or you could have your delegate add the suffix after editing is done, like:
#interface MyViewController : UIViewController <UITextFieldDelegate> {...}
...
[textField setDelegate:self];
...
- (void)textFieldDidEndEditing:(UITextField *)textField {
NSString *textWithSuffix = [[NSString alloc] initWithFormat:#"%#%#", [textField text], #"SUFFIX"];
[textField setText:textWithSuffix];
[textWithSuffix release];
}

Resources