How does replacingCharacters work in a delegate? - ios

I'm new to Swift and I'm learning about delegates:
class ZipCodeTextFieldDelegate: NSObject, UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var newText = textField.text! as NSString
newText = newText.replacingCharacters(in: range, with: string) as NSString // this line
return newText.length <= 5
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true;
}
}
This is basically a delegate to limit the character count to within 5. But, I'm not quite sure what role this method replacingCharacters is playing in this function. Apple documentation shows that it
Returns a new string in which the characters in a specified range of
the receiver are replaced by a given string.
which seems pretty straight forward. It replaces some range of characters with a replacement of your choice. I can see that textField has a parameter called range and replacementString, but what range is being provided and what are they being replaced with?
The main ViewController that the delegate is being used in doesn't provide either of them. It simply instantiate the delegate and applies it to the relevant textField
let zipCodeDelegate = ZipCodeTextFieldDelegate()
// MARK: Outlets
#IBOutlet weak var textField: UITextField!
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self.zipCodeDelegate
}

First, you should understand the purpose of shouldChangeCharactersIn. Whenever the user tries to change the text of a text field in some way, the text field asks its delegates whether it "should change the characters", i.e. whether it should allow the user to do the change that they are trying to do.
Note that the user can change the textfield's text in a lot of ways, not just by "entering a single character". Here's a list of possible ways (you might be able to think of others):
entering a single character, or pasting some text
deleting a single character
selecting a range of characters, and deleting it
selecting a range of characters, and entering a single character, or pasting some text. This will replace the selected range with the single character, or pasted text
Notice that all those ways are just special cases of Way 4. In all of these ways, you are just replacing a range in the textfield's text with some new text. Let me reword the first 3 ways:
replacing a zero-length range at some position with a single character, or some pasted text
replacing a one-length range of the textfield with an empty string
replacing some range of the textfield with an empty string
When the user tries to change the textfield's text in any way, the textfield will make a call like similar to this:
delegate?.textField(self, shouldChangeCharactersIn: someRange, replacementString: someString)
You should now understand what the parameters someRange and someString are for. No matter how the user changes the text, it can always be modelled as "replacing a range of text (someRange) with some other text (someString)".
So what does
newText.replacingCharacters(in: range, with: string)
do?
It simply does the replacement mentioned before. Recall that this delegate method is the textfield asking you, "should this change (modelled as a range and a replacement string) be allowed?" And to determine that, you try to do the change first. If after the change, the text field's text has a length of at most 5, then it is allowed.
The line tries to do the change.
Of course, you are not changing the textfield's text when doing this change, which is why you assign it to a variable called newText.

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

How to simulate a key press when another key is pressed?

I am trying to get the delete key to do what the spacebar normally does. In other words, when the user clicks the delete in a UITextField, I want a space to appear instead.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let char = string.cString(using: String.Encoding.utf8)!
let isBackSpace = strcmp(char, "\\b")
if (isBackSpace == -92) {
print("Delete was pressed")
return false
}
return true
}
I know that this only disables the delete key, but how would I move forward? Also, the space would replace the selected text (there is always some text that is selected).
The textField parameter is the text field in which the user is working. You are free to do there whatever you like. So, in addition to returning false, and just before doing so, change the text of the textField to whatever you want.
If what you want to do is append a space to the text, an easy way is to fetch textField.text!, append a space to that string, and set textField.text. Or if you want to replace the current selection with a space, instead of letting it be removed, do that to textField.text, using the range parameter to tell you where the space should go.
In other words, if you know anything at all about how to work with strings, you can do that to the string that is textField.text as part of this method.
There are other ways using the UITextInput protocol, which allows you to pretend to be the keyboard. For example, just saying textField.insertText(" ") might give you desired result, but you'll have to try it and see.

Which delegate method is called when i manually assign text to a UITextField in iOS Swift 3

let me explain my question with an example
I have a UITextField and want to limit its text and also have a UILabel that tells how many more characters can the user type.
Thus when I start typing, everything is running great, I am using this delegate function:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {}
But when i assign text like this : textField.text = "Hello",
the above mention method is not called and thus the counter label value is not changed,
I have also tried: textField?.addTarget(self, action: #selector(txtFieldChanged), for: .allEditingEvents)
but no luck
According to you question when you are assigning text to your textfield like "hello" then you have to calculate the textfield length in your view did load method and set the counter there. And when you will start typing in you text field your delegate will be called.
double left = 500 - textView.text.length ;
lblcounter.text = [NSString stringWithFormat:#"%.0f character left",left];

Test UITextField text string to only contain alphanumeric characters

Im trying to complete form validation in Swift and cant find a way of testing for only Alphanumeric characters in a UITextField.text.
Ive found NSCharacterSet to help test if at least 1 letter has been entered (so far):
#IBOutlet weak var username: UITextField!
let letters = NSCharacterSet.letterCharacterSet()
//Check username contains a letter
if (username.text!.rangeOfCharacterFromSet(letters) == nil) {
getAlert("Error", message: "Username must contain at least 1 letter")
}
Now i just need a way to validate that only numbers, letters (maybe even underscores and dashes) to be entered. Loads of stuff out there for Obj-C but I need a SWIFT solution please.
Thank you in advance.
Check if the inversion of your accepted set is present:
if username.text!.rangeOfCharacterFromSet(letters.invertedSet) != nil {
print("invalid")
}
letters should probably be alphanumericCharacterSet() if you want to include numbers as well.
If you want to accept underscores or more chars, you will probably have to create a character set by your own. But the inversion logic will stay the same.
Though it's late to answer, but this answer might be useful to someone.
This is simple and worked like a charm for me.
Swift 3:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
/// 1. replacementString is NOT empty means we are entering text or pasting text: perform the logic
/// 2. replacementString is empty means we are deleting text: return true
if string.characters.count > 0 {
var allowedCharacters = CharacterSet.alphanumerics
/// add characters which we need to be allowed
allowedCharacters.insert(charactersIn: " -") // "white space & hyphen"
let unwantedStr = string.trimmingCharacters(in: allowedCharacters)
return unwantedStr.characters.count == 0
}
return true
}
Note: This will work for pasting strings into the text field as well. Pasted string will not be displayed in text field if it contains any unwanted characters.

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