Preventing Cursor Move when Validation Fails - ios

I would like to implement validation in my view such that, when the user enters invalid text into a UITextField, all other UITextFields are disabled and the cursor remains in the current place. I am using the SwiftValidator library (https://github.com/jpotts18/SwiftValidator).
The validation of a text field is triggered by UIControlEvent.EditingDidEnd, which typically happens when the user moves to the next field. I have not been able to prevent the cursor moving to the other UITextField nor have I been able to get it back to the view that failed validation. What would be the right approach?
Below are the relevant sections of my code, where validateSingleValue() is the place where I'm trying to implement the logic for this task.
My ViewController acts as a delegate to my custom control groups, handling EditingDidEnd events. The custom controls are typically a UILabel and a UITextField embedded in a UIView.
func textViewEditingDidEnd(editTextPropertyGroup: EditTextPropertyGroup) {
validateField(editTextPropertyGroup.propertyValue)
}
Fields are validated using
func validateField(sender: AnyObject) {
switch (sender as! UITextField) {
case firstNameField:
validateSingleValue(textfield: firstNameField, validationrule: firstNameRule(), errorlabel: firstNameErrorLabel, container: firstNameField.superview)
case lastNameField:
validateSingleValue(textfield: lastNameField, validationrule: lastNameRule(), errorlabel: lastNameErrorLabel, container: lastNameField.superview)
case adressField:
validateSingleValue(textfield: addressField, validationrule: addressRule(), errorlabel: addressErrorLabel, container: addressField.superview)
case emailField:
validateSingleValue(textfield: emailField, validationrule: emailRule(), errorlabel: emailErrorLabel, container: emailField.superview)
default:
print("No fields to validate.")
}
}
func validateSingleValue(textfield textField:UITextField, validationrule validationRule: Rule, errorlabel errorLabel: UILabel, container: UIView?) -> Bool {
if validationRule.validate(textField.text!) {
errorLabel.hidden = true
textField.layer.borderColor = UIColor.grayColor().CGColor
textField.layer.borderWidth = 0.5
enableAllControls()
return true
} else {
errorLabel.hidden = false
textField.layer.borderColor = UIColor.redColor().CGColor
textField.layer.borderWidth = 1.0
if let unwrappedContainer = container {
textField.becomeFirstResponder()
disableAllControls()
unwrappedContainer.userInteractionEnabled = true
unwrappedContainer.alpha = 1.0
}
return false
}
}
and for disabling and enabling the controls:
override func disableAllControls() {
self.FirstNameSection.propertyValue.userInteractionEnabled = false
self.FirstNameSection.alpha = 0.3
self.LastNameSection.userInteractionEnabled = false
self.LastNameSection.alpha = 0.3
self.AddressSection.userInteractionEnabled = false
self.AddressSection.alpha = 0.3
self.EmailSection.userInteractionEnabled = false
self.EmailSection.alpha = 0.3
}
override func enableAllControls() {
self.FirstNameSection.propertyValue.userInteractionEnabled = true
self.FirstNameSection.alpha = 1.0
self.LastNameSection.userInteractionEnabled = true
self.LastNameSection.alpha = 1.0
self.AddressSection.userInteractionEnabled = true
self.AddressSection.alpha = 1.0
self.EmailSection.userInteractionEnabled = true
self.EmailSection.alpha = 1.0
}

Jerry, I did something like this many moons ago; this is a snippet of the code in objective C I fear, sorry.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
if ([textField.accessibilityIdentifier isEqualToString:#"ClassSize"]) {
if ([self isNumeric:string]) {
return YES;
} else {
return NO;
}
}
return YES;
}
Basically if the user tried to input incorrect data, the keyboard didn't respond.
I used this technique to guard agains't number that were beyond a certain range and indeed even to restrict said user when entering a hexadecimal number I recall. They could only enter 0-F in the field.

It turns out that all this can be achieved with only a few lines of code. To support validation, the UITextFieldDelegate protocol defines the following methods:
textFieldShouldBeginEditing(_:)
textFieldDidBeginEditing(_:)
textFieldShouldEndEditing(_:)
textFieldDidEndEditing(_:)
(see documentation here)
The code in the question can be reduced to:
var shouldEndEdit: Bool = true
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
validator.validateField(textField) { error in
if error == nil {
self.shouldEndEdit = true
} else {
self.shouldEndEdit = false
}
}
return self.shouldEndEdit
}
plus the validation rules as described here.

Related

Detect when a button is pressed outside of the IBAction in Swift?

I'm a beginner programmer making my first real app, a calculator in Swift.
It's mostly working, but one issue I'm running into is how it displays numbers after pressing one of the operator buttons. Currently, whenever an operator button is pressed, I have it set the label at the top of the calculator to "0". But on actual calculators, this top display won't change until another number button is pressed.
If I don't reset the display to 0, then any number buttons that are pressed will add to the current text at the top, and mess up the equation that the calculator will have to do (i.e. 2+2 displays 22, and the solution it displays is 22+2=24)
I'm wondering if it's possible to detect when one of the number buttons is pressed (listed in my code as the intButtonPressed IBAction) outside of the intButtonPressed function? That way I can keep the top label the same until the user starts inputting more text, then I can set it to 0 to prevent the calculator from breaking.
Any other possible (better) solutions would be welcome as well
Here's my code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var topLabel: UILabel!
var num1 = Double()
var solution = Double()
var op = String()
#IBAction func intButtonPressed(_ sender: UIButton) {
if topLabel.text == "0" {
topLabel.text = sender.currentTitle
}
else if topLabel.text == String(solution) {
num1 = Double(topLabel.text!)!
solution = 0.00
topLabel.text = sender.currentTitle
// Basically stops user from adding to solution?
// Also stores solution as num1 and clears solution field
}
else {
topLabel.text = topLabel.text! + sender.currentTitle!
// Keep typing
}
if (topLabel.text?.count)! > 12 {
topLabel.text = "Error"
}
else {
return
}
// if its greater than 12 characters, display "error"
}
#IBAction func clearButtonPressed(_ sender: UIButton) {
num1 = 0.00
solution = 0.00
topLabel.text = "0"
}
#IBAction func opButtonPressed(_ sender: UIButton) {
if sender.currentTitle == "=" {
equals()
}
else {
op = sender.currentTitle!
num1 = Double(topLabel.text!)!
topLabel.text = "0"
}
// Need it to display num1 until I press another button, then I need it to only display that text.
}
func equals() {
switch op {
case "+":
add()
case "-":
subtract()
case "×":
multiply()
case "÷":
divide()
default:
print(Error.self)
}
}
func add() {
solution = num1 + Double(topLabel.text!)!
topLabel.text = String(solution)
}
func subtract() {
solution = num1 - Double(topLabel.text!)!
topLabel.text = String(solution)
}
func multiply() {
print("topLabel = ", topLabel.text!)
solution = num1 * Double(topLabel.text!)!
print("solution = ", solution)
topLabel.text = String(solution)
}
func divide() {
solution = num1 / Double(topLabel.text!)!
//answer()
}
}
Update for anyone who finds this in the future: I've solved the issue, and realized that I wasn't very clear with what I wanted it to do. I solved the problem simply by adding a condition to the if/else statement in the inButtonPressed function that detects if the topLabel is 0. By rewriting that to detect if the topLabel is 0 OR the same as num1, and then changing the else statement in the opButtonPressed function to set topLabel to String(num1), the program does exactly what I want. Here's the rewritten code:
#IBAction func intButtonPressed(_ sender: UIButton) {
if topLabel.text == "0" || topLabel.text == "0.0" || topLabel.text == String(num1){
dotPressed = false
// Resets dotPressed whenever the top is 0 or num1 and an int button is pressed
topLabel.text = sender.currentTitle
}
else if topLabel.text == String(solution) {
num1 = Double(topLabel.text!)!
solution = 0.00
topLabel.text = sender.currentTitle
// Basically stops user from adding to solution?
// Also stores solution as num1 and clears solution field
}
else {
topLabel.text = topLabel.text! + sender.currentTitle!
}
}
#IBAction func opButtonPressed(_ sender: UIButton) {
if sender.currentTitle == "=" {
equals()
}
else {
op = sender.currentTitle!
num1 = Double(topLabel.text!)!
topLabel.text = String(num1)
}
// Successfully displays num1 until I press another button, then only displays that text.
I also added a separate function to handle decimals, and I had to update the code in there as well to keep that working.

How to add two numbers in Swift only when both of the text fields are filled in

I'm trying to add two numbers in Swift 5, and I want to add some error checks. I don't want to make it possible for a user to click on the plus button if both of the text fields are not filled in. I tried with the if state below but it did not work.
the whole function:
#IBAction func sum(_ sender: Any) {
let one = input1.text
let oneInt = Int(one!)
let two = input2.text
let twoInt = Int(two!)
let total = oneInt! + twoInt!
label.text = "\(total)"
if(input2.text == nil){
addBtn.isEnabled = false
}
if(input1.text == nil){
addBtn.isEnabled = false
}
}
Try to use guard like this. If your input field does not contain any value that field return blank string and when you try to get integer value from that string it will return nil and your add button will be disable.
#IBAction func sum(_ sender: Any) {
guard let text1 = input1.text, let intValue1 = Int(text1) else {
addBtn.isEnabled = false
return
}
guard let text2 = input2.text, let intValue2 = Int(text2) else {
addBtn.isEnabled = false
return
}
label.text = "\(intValue1 + intValue2)"
}
A nice and simple way is to addTarget to your textFiels. This will enable you to handle the events on the text field. In this scenario we'll use .editingChanged and use a single selector to achieve our goal:
What we'll do : We will listen for when someone types something in the textfield. Whenever a text changed was made, we'll check to see if all the textfields was populated and then if it was we enable the sum button.
A small controller sample :: Make sure to read the comments to understand the code faster
import UIKit
class ViewController: UIViewController {
#IBOutlet var textfield1: UITextField!
#IBOutlet var textfield2: UITextField!
#IBOutlet var sumButton: UIButton!
#IBOutlet weak var label: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
sumButton.isEnabled = false /// Disable the button first thing
[textfield1, textfield2].forEach {
$0.addTarget(self, action: #selector(editingChanged(_:)), for: .editingChanged) /// add targets to handle the events (in your case it listens for the 'editingChanged' event )
}
}
#objc func editingChanged(_ textField: UITextField) {
/// Here we just loop through all our textfields
for each in [textfield1, textfield2] {
if let text = each?.text { /// Just make sure the textfields text is not nil
if text.count < 1 {
// If the textfiels text has no value in, then we keep the button disabled and return
sumButton.isEnabled = false
return
}
} else {
/// Else if the text field's text is nill, then return and keep the button disabled
sumButton.isEnabled = false
return
}
}
sumButton.isEnabled = true /// If the code reaches this point, it means the textfields passed all out checks and the button can be enabled
}
#IBAction func sum(_ sender: Any) {
let one = textfield1.text!
let two = textfield2.text!
guard let oneInt = Int(one), let twoInt = Int(two) else {
print("Whatever was in that text fields, couldn't be converted to an Int")
label.text = "Be sure to add numbers."
return
}
let total = oneInt + twoInt
label.text = "\(total)"
}
}
textfields are not nil but empty strings. so make your comparison like :
if input1.text == "" {
// do your check here
}
Seems like you want to start with addBtn.isEnabled = false then update it whenever the user enters two valid integers into the text fields, i.e. Int(input1.text ?? "") != nil && Int(input2.text ?? "") != nil. You can do this by adding a target to your textfields (input1 and input2) for .editingChanged events. For example, if you're doing this in a UIViewController, you can do this in viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
addBtn.isEnabled = false
input1.addTarget(self, action: #selector(textFieldDidEdit(_:)), for: .editingChanged)
input2.addTarget(self, action: #selector(textFieldDidEdit(_:)), for: .editingChanged)
}
Where textFieldDidEdit(_:) action looks like:
#objc func textFieldDidEdit(_ sender: UITextField) {
addBtn.isEnabled = Int(input1.text ?? "") != nil && Int(input2.text ?? "") != nil
}
Finally your sum function becomes:
#IBAction func sum(_ sender: UIButton) {
guard let oneInt = Int(input1.text ?? ""), let twoInt = Int(input2.text ?? "") else {
return
}
let total = oneInt + twoInt
label.text = "\(total)"
}
As all of the number validation has moved to the textFieldDidEdit(_:) function.

MDCTextInputController error messages cover entered text

Using material-components/material-components-ios v85.8.0
import MaterialComponents
....
var usernameTextField = MDCTextField()
var userNameTextLayout = MDCTextInputControllerUnderline()
usernameTextField = {
let usernameTextEdit = MDCTextField()
usernameTextEdit.translatesAutoresizingMaskIntoConstraints = false
usernameTextEdit.clearButtonMode = .unlessEditing
usernameTextEdit.backgroundColor = .white
return usernameTextEdit
}()
userNameTextLayout.textInput = usernameTextField
userNameTextLayout.placeholderText = "Username"
// add to view
....
private func isUserNameValid() -> Bool {
let enteredUsername = usernameTextField.text ?? ""
if (!enteredUsername.isValidEmail) {
userNameTextLayout.setErrorText("Invalid e-mail address",
errorAccessibilityValue: nil)
return false
}
}
Error messages cover the text entered and it looks bad:
Android material design does however place the error underneath the line:
Was wondering if there's a way to do that, or if I'm doing it wrong.
I followed their tutorial: https://codelabs.developers.google.com/codelabs/mdc-101-swift/#2
I had set the height of the MDCTextField too small. In increasing it from 50->80pts it did the trick and moved the error message below the line.

I am not sure why my else if is not being called in my IBAction

I have created an IBAction that handles 2 buttons, first one's title is "True" and the second one's title is "False", when tapped they should display a label however the else if never gets called.
#IBAction func trueOrFalse(sender: UIButton) {
if sender.currentTitle == "True" {
answerR.hidden = false
answerW.hidden = true
} else if sender.currentTitle == "False" {
print("hi")
answerW.hidden = false
answerR.hidden = true
}
}
answerW and answerR are the labels.
I am not sure why, I have tried a couple of things like using taps but I can not seem to figure it out.
Sounds like one of your buttons isn't registering the IBAction - double check and make sure each are connected via Touch Up Inside. If they are connected w/ a different method it may not register as you expect!
You should use button tags. It is very simple like below:
Firstly, you should set each button tag like below:
Buton1 Tag = 0
Buton2 Tag = 1
Then use below code:
#IBAction func trueOrFalse(sender: UIButton) {
if sender.tag == 0 {
answerR.hidden = false
answerW.hidden = true
}else
{
print("hi")
answerW.hidden = false
answerR.hidden = true
}
}
you need to eliminate optional that comes with string
#IBAction func trueOrFalse(sender: UIButton) {
if (sender.currentTitle!)! == "True" {
answerR.hidden = false
answerW.hidden = true
} else if (sender.currentTitle!)! == "False" {
print("hi")
answerW.hidden = false
answerR.hidden = true
}
}

UITextField has trailing the whitespace after secureTextEntry toggle

I am trying to toggle the password text field, but I am facing a problem of white space at right side of text field.
//Eye button action.
#IBAction func btnEyePassword(sender: AnyObject)
{
//If password text is secure.
if (self.passrordText.secureTextEntry == true)
{
self.passrordText.secureTextEntry = false
}
else
{
self.passrordText.secureTextEntry = true
}
}
Swift 4 solution
func togglePasswordVisibility() {
password.isSecureTextEntry = !password.isSecureTextEntry
if let textRange = password.textRange(from: password.beginningOfDocument, to: password.endOfDocument) {
password.replace(textRange, withText: password.text!)
}
}
You don't need extra if statement for simple toggle isSecureTextEntry property, but you should replace text this way to force UITextField recalculate text width to avoid extra space.
UPDATE
Swift 4.2
Instead of
password.isSecureTextEntry = !password.isSecureTextEntry
you can do this
password.isSecureTextEntry.toggle()
Dont worry, in both cases when you print your textField.text your data will be same, there would not be white space.
Check with print(self.passrordText.text)
Declare var str : String = ""
#IBAction func btnEyePassword(sender: AnyObject)
{
//If password text is secure.
if (self.passrordText.secureTextEntry == true)
{
self.passrordText.secureTextEntry = false
self.passrordText.text = ""
self.passrordText.text = str
print(self.passrordText.text)
}
else
{
self.passrordText.secureTextEntry = true
str = self.passrordText.text!
print(self.passrordText.text)
}
}
This happens because of space consumption, "W" keeps much space while "i" keep lesser space.

Resources