shouldChangeCharactersInRange multiple text fields gets frozen - swift

Updated code bellow with solution.
This works with as many fields you want.
It also fixes the textfield frozen issue when the method returns false.
The line bellow is what guides the method to return true after returns false.
newString = currentString.stringByReplacingCharactersInRange(range, withString: string)
On view did load (this will add an identifier to each field so you can identify within the method what field is being used - it is an Int())
emailAddressField.tag = 1
userPasswordField.tag = 2
On delegate method
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
var maxLength = Int()
var newString = NSString()
var currentString = NSString()
println("MY TAG \(textField.tag)")
switch textField.tag {
case 1:
println("CASE 1 \(range)")
maxLength = 16
currentString = emailAddressField.text
newString = currentString.stringByReplacingCharactersInRange(range, withString: string)
case 2:
println("CASE 2 \(range)")
maxLength = 8
currentString = userPasswordField.text
newString = currentString.stringByReplacingCharactersInRange(range, withString: string)
default:
println("Didn't detect any field")
}
return newString.length <= maxLength
}

The issue is that the delegate method shouldChangeCharactersInRange is used for both text fields and in your implementation you're returning false as soon as one of the text fields reaches its limit which ultimately makes both text fields deny further input. To resolve this you need to check the method's textField parameter to identify which text field the method is called for.
One of the possible ways to do this is to set up tags on your two text fields, for instance in viewDidLoad,
override func viewDidLoad() {
...
emailAddressField.tag = EMAIL_ADDRESS_TEXTFIELD // e.g. 0
userPasswordField.tag = USER_PASSWORD_TEXTFIELD // e.g. 1
}
and then act upon the tag of the text field supplied to the delegate method
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let newString = textField.text!.stringByReplacingCharactersInRange(range, withString: string)
if textField.tag == EMAIL_ADDRESS_TEXTFIELD && count(newString) + 1 == 30 {
return false
}
if textField.tag == USER_PASSWORD_TEXTFIELD && count(newString) + 1 == 11 {
return false
}
return true
}

Related

How can I rewrite this in swift 4 or 5

I get an error saying 'characters' is unavailable: Please use String directly.
extension LoginViewController : UITextFieldDelegate {
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
if let username = self.usernameField.text,
let password = self.passwordField.text {
if ((username.characters.count > 0) && //This is where I get the error
(password.characters.count > 0)) { //This is where I get the error
self.loginButton.isEnabled = true
}
}
return true
}
}
How can I fix this?
Simply remove .characters
extension LoginViewController : UITextFieldDelegate {
func textField(_ textField: UITextField,
shouldChangeCharactersIn range: NSRange,
replacementString string: String) -> Bool {
if let username = self.usernameField.text,
let password = self.passwordField.text {
if username.count > 0 &&
password.count > 0 {
self.loginButton.isEnabled = true
}
}
return true
}
}
you don't need to state characters to count since it will already be counted when you type username.count or password.count
if ((username.count > 0) &&
(password.count > 0)) {
In Swift never check for empty string or empty collection type with .count > 0, there is the dedicated method isEmpty.
And in Swift parentheses around if expressions are not needed and the && operator can be replaced with a comma
if !self.usernameField.text!.isEmpty, !self.passwordField.text!.isEmpty {
self.loginButton.isEnabled = true
}
The text property of UITextField is never nil so force unwrapping is fine.

Not able to filter object array

I was trying to filter my array of objects by one of its properties. tried lot of solutions but it was throwing error after I start typing
//My model class
class Book{
var bookId:Int?
var bookName:String?
//omitted init function
}
// viewController
//this is my textfield delegate method
let myArray:[Book] = [Book(bookId:23,bookName:"book1"),Book(bookId:53,bookName:"book2"),Book(bookId:43,bookName:"book3"),]
func textField(_ textField: UITextField, shouldChangeCharactersIn
range: NSRange, replacementString string: String) -> Bool
{
lastCharactar = string
textFieldText = myTextField.text! + lastCharactar
let predicate = NSPredicate(format: "ANY SELF.bookName BEGINSWITH %#", textFieldText)
let arr = ( myArray as NSArray).filtered(using: predicate)
return true
}
I am getting the following error
"this class is not key value coding-compliant for the key bookName."
Swift Array doesn't need predicate to filter its content. Swift array has filter method to filter array. Eg:
struct Book{
var bookId:Int?
var bookName:String?
}
let myArray:[Book] = [Book(bookId:23,bookName:"book1"),Book(bookId:53,bookName:"book2"),Book(bookId:43,bookName:"book3"),]
func textField(_ textField: UITextField, shouldChangeCharactersIn
range: NSRange, replacementString string: String) -> Bool
{
lastCharactar = string
textFieldText = myTextField.text! + lastCharactar
let arr = myArray.filter { (book) -> Bool in
if let name = book.bookName, name.hasPrefix(textFieldText) {
return true
}
return false
}
return true
}
Note: A struct is a value type whose value is copied when it is assigned to a variable or constant, or when it is passed to a function whereas class is reference type whose values will not get copied by default.
The bridge cast to NSArray to apply the predicate requires key-value compliance of the properties by adding the #objc attribute and the class must be even a subclass of NSObject.
But this is Swift, there's no need to use NSPredicate and NSArray. This native syntax does the same
let arr = myArray.filter{ $0.bookName.range(of: textFieldText, options: .anchored) != nil }
Side note:
Apparently all books have name and id so declare the object as struct with non-optional constant members and remove the redundant naming. The initializer is for free.
struct Book {
let id : Int
let name : String
}
var myArray = [Book(bookName:"book1", bookId:23),Book(bookName:"book2", bookId:53),Book(bookName:"book3", bookId:43)]
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
var lastCharactar = string
var textFieldText = myTextField.text! + lastCharactar
let arr = myArray.filter{ $0.bookName!.range(of: textFieldText, options: .caseInsensitive) != nil }
print(arr)
return true
}
struct Book {
var bookName : String?
var bookId : Int?
}

Limitate number of string to type in TextField - Swift

I want to limitate my users to type just 3 words inside my TextField. I found some other topic that are talking about to limitate the char in input, but it's not what I want.
Does someone know if it's possible to count the space inside the TextField and do a kind of if statement if more than 3 spaces are typed you can't write anything else?
Ex: "Boys rescued in Thailand" should be blocked after "Boys rescued in"
Thanks in advance!
The quickest solution would be to hook into textField's delegate method shouldChangeCharactersIn
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text, text.split(separator: " ").count < 3 {
return true
}
return (string == "" || string != " ")
}
I bealeave you could have custom validation function that would take the string from field then, split it to array. And check the length of array.
var fullName = "First Last"
var fullNameArr = split(fullName) {$0 == " "}
var firstName: String = fullNameArr[0]
var lastName: String? = fullNameArr.count > 1 ? fullNameArr[1] : nil
In TextField Delegate method
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
do some check like this
var wordCount = 0
if let text = textField.text {
wordCount = text.split(separator: " ").count
}
if wordCount < 3 {
return true
}
return false

Switching to next UITextField when character limit is reached

I have three text fields to enter in a phone number. I am trying to set the character limit for each textfield to three characters and once this is reached switch to a new textfield.
I saw online to use this code to limit the characters:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let currentCharacterCount = textField.text?.characters.count ?? 0
if (range.length + range.location > currentCharacterCount){
return false
}
let newLength = currentCharacterCount + string.characters.count - range.length
return newLength <= 25
}
and to use this to switch to typing on a new textfield:
.didbecomefirstresponder()
but I am not sure how to limit a text field to 3 characters and then switch to the next field.
This is my code, how I solve this:
The three textfields are:
#IBOutlet var txtField1: UITextField!
#IBOutlet var txtField2: UITextField!
#IBOutlet var txtField3: UITextField!
import UITextFieldDelegate in to your class and set the delegate to self for all the textfields.
And use this method to change focus.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let currentCharacterCount = ((textField.text?.characters.count)! + string.characters.count) - 1
switch (textField, currentCharacterCount) {
case (self.txtField1, 3):
self.txtField2.becomeFirstResponder()
case (self.txtField2, 7):
self.txtField3.becomeFirstResponder()
default:
break
}
return true
}
Here I set character count 3 for the first textfield, and 7 for second textfield.
You could use the delegate method for UITextField shouldChangeCharactersInRange. You'd have to do a little setup work though. Here's an example that creates 3 textFields, conforms to the UITextFieldDelegate, and does something similar to what you're describing.
class ViewController: UIViewController, UITextFieldDelegate {
var max = 3
var fields:[UITextField] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
// This is just an example to layout 3 text fields.
for i in 0 ..< 3 {
let field = UITextField()
view.addSubview(field)
field.delegate = self
field.borderStyle = .roundedRect
field.font = UIFont(name: "AvenirNext-Regular", size: 15.0)
field.textColor = UIColor.black
field.frame = CGRect(x: view.bounds.width/2 - 100, y: 100 + (150*CGFloat(i)), width: 200, height: 50)
fields.append(field)
}
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
// Make sure the field has non-nil text
if let text = textField.text {
// Check if the text length has reached max limit
if text.characters.count > (max-1) {
// Get index of field from fields array
if let index = fields.index(of: textField) {
// Make sure it's not the last field in the array else return false
if index < fields.count-1 {
// Make the next field in the array become first responder if it's text is non-nil and it's text's character count is less than desired max
if fields[index+1].text != nil && fields[index+1].text!.characters.count < (max-1) {
fields[index+1].becomeFirstResponder()
} else {
return false
}
} else {
return false
}
}
}
}
return true
}
}
I find Pierce's answers too complex and anas.p's answer doesn't work for me like it's supposed to. My Swift 5 solution:
override func viewDidLoad() {
textField1.addTarget(self, action: #selector(textFieldDidChange1), for:.editingChanged)
textField2.addTarget(self, action: #selector(textFieldDidChange2), for:.editingChanged)
}
#objc func textFieldDidChange1() {
// After 2 characters are typed, immediately jump to textField2
if (textField1.text?.count == 2) {
self.textField2.becomeFirstResponder()
}
}
#objc func textFieldDidChange2() {
if (textField2.text?.count == 2) {
self.textField3.becomeFirstResponder()
}
}

Digit input in a text field

Additional question:
Still need some help with my code. The textfield is 'measuredValue' and I plan to have 30 different texfields (measuredValue1...30). When I type '923' the text will be set to '9.23' right away. Then I want to add '4'... for '92.34' but that doesn't work. Thanks for helping out.
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String)
-> Bool {
if count(string) == 0 { return true }
var measuredValue = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
switch textField {
case digitsOnlyTextField:
if count(measuredValue) == 3 || count(measuredValue) == 4 {
let stringNumeric = Double(measuredValue.toInt()!) / 100
measuredValue = String(format:"%.2f", stringNumeric)
digitsOnlyTextField.text = measuredValue
}
return measuredValue.containsOnlyCharactersIn("0123456789") && count(measuredValue) <= 4
default:
return true
}
}
Original question:
I would like to validate my text fields to get the right input for my app. Input needs to be formatted like '9.90' or '15.34'. So always 3 or 4 digits, and always 2 decimals.
I would like to use 'numberpad keyboard' (just 0...9, no point) and add the decimal point after the user exits the field. So the user input is 990 or 1534, and then in the text field it will become 9.90 or 15.34 automatically.
I tried searching for examples first, but didn't find what I was looking for.
Any help appreciated.
Jan
You have to implement the UITextField delegate method
func textFieldDidEndEditing(textField: UITextField) {
//Logic goes here....
var textString = textField.text as! String
let textLength = countElements(textString)
if textLength >= 3 && textLength <= 4 {
let stringNumeric = Double(textString.toInt()!) / 100
let texts = String(format:"%.2f", stringNumeric)
textField.text = texts
}
}
Your class should confirm to the protocol UITextFieldDelegate
this is the 'final' code I used (but I'm open for improvements!). A few remarks:
when a user clicks the background (and leaves the textfield) the
decimal point gets set.
when a user copies & pastes text from another app, this code handles that.
when a user goes back in the textfield for editing, the decimal point gets set again.
Deleting a value (to an empty textfield) is handled correctly.
So I'm pleased. Thanks for the help.
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var numberField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
numberField.delegate = self
numberField.keyboardType = UIKeyboardType.NumberPad
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// Tap background to add decimal point and defocus keyboard
#IBAction func userTappedBackground(sender: AnyObject) {
for view in self.view.subviews as! [UIView] {
if let textField = view as? UITextField {
if count(numberField.text) > 0 {
var numberString = numberField.text
numberString = numberString.stringByReplacingOccurrencesOfString(".", withString: "", options: NSStringCompareOptions.LiteralSearch, range: nil)
var numberFromString = Double(numberString.toInt()!) / 100
numberField.text = String(format:"%.2f", numberFromString)
}
textField.resignFirstResponder()
}
}
}
func textField(textField: UITextField,
shouldChangeCharactersInRange range: NSRange,
replacementString string: String)
-> Bool {
var result = true
var prospectiveText = (textField.text as NSString).stringByReplacingCharactersInRange(range, withString: string)
prospectiveText = prospectiveText.stringByReplacingOccurrencesOfString(".", withString: "", options: NSStringCompareOptions.LiteralSearch, range: nil)
if textField == numberField {
if count(string)>0 {
let disallowedCharacterSet = NSCharacterSet(charactersInString: "0123456789").invertedSet
let replacementStringIsLegal = string.rangeOfCharacterFromSet(disallowedCharacterSet) == nil
let resultingStringLengthIsLegal = count(prospectiveText) <= 4
let scanner = NSScanner(string: prospectiveText)
let resultingTextIsNumeric = scanner.scanDecimal(nil) && scanner.atEnd
result = replacementStringIsLegal && resultingStringLengthIsLegal && resultingTextIsNumeric
}
}
return result
}