21

I am having trouble figuring out how to change my code to make it so the Done button in the navigation bar is enabled when my three text fields are filled out.

I currently have three UITextFields and one UIButtonItem. Both the habitNameField and the goalField are manual text fields, and the frequencyField is a Picker View.

@IBOutlet weak var habitNameField: UITextField!
@IBOutlet weak var goalField: UITextField!
@IBOutlet weak var frequencyField: UITextField!

@IBOutlet weak var doneBarButton: UIBarButtonItem!

I also have the following function that works when there is something typed in the first field.

func textField(habitNameField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let oldHabitNameText: NSString = habitNameField.text!
    let newHabitNameText: NSString = oldHabitNameText.stringByReplacingCharactersInRange(range, withString: string)
    doneBarButton.enabled = (newHabitNameText.length != 0)
    return true
}

I tried change the code so that it took in the other two fields as parameters and enabled the doneBarButton only if all three fields were filled out.

func textField(habitNameField: UITextField, goalField: UITextField, frequencyField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let habitNameText: NSString = (habitNameField.text!).stringByReplacingCharactersInRange(range, withString: string)
    let goalText: NSString = (goalField.text!).stringByReplacingCharactersInRange(range, withString: string)
    let frequencyText: NSString = (frequencyField.text!).stringByReplacingCharactersInRange(range, withString: string)

    doneBarButton.enabled = (habitNameText.length != 0) && (goalText.length != 0) && (frequencyText.length != 0)
    return true
}

However, it's not working, even when I fill out all three text fields.

I would really appreciate any help, and thanks to anyone who contributes in advance!

All code here:

class HabitDetailViewController: UITableViewController, UITextFieldDelegate, UIPickerViewDataSource,UIPickerViewDelegate {
@IBOutlet weak var habitNameField: UITextField!
@IBOutlet weak var goalField: UITextField!
@IBOutlet weak var doneBarButton: UIBarButtonItem!
@IBOutlet weak var frequencyField: UITextField!

var frequencies = ["Day", "Week", "Month", "Year"]
var frequencyPicker = UIPickerView()

var habitToEdit: HabitItem?
weak var delegate: HabitDetailViewControllerDelegate?

@IBAction func cancel() {
    delegate?.habitDetailViewControllerDidCancel(self)
}

@IBAction func done() {
    print("You plan to do \(habitNameField.text!) \(goalField.text!) times a \(frequencyField.text!.lowercaseString).")
    if let habit = habitToEdit {
        habit.name = habitNameField.text!
        habit.numberLeft = Int(goalField.text!)!
        habit.frequency = frequencyField.text!
        delegate?.habitDetailViewController(self, didFinishEditingHabit: habit)
    } else {
        let habit = HabitItem()
        habit.name = habitNameField.text!
        habit.numberLeft = Int(goalField.text!)!
        habit.frequency = frequencyField.text!
        habit.completed = false
        delegate?.habitDetailViewController(self, didFinishAddingHabit: habit)
    }
}

override func viewWillAppear(animated: Bool) {
    super.viewWillAppear(animated)
    habitNameField.becomeFirstResponder()
    frequencyPicker.hidden = false
}

override func viewDidLoad() {
    super.viewDidLoad()
    frequencyPicker.dataSource = self
    frequencyPicker.delegate = self
    doneBarButton.enabled = false
    habitNameField.addTarget(self, action: "checkFields:", forControlEvents: .EditingChanged)
    goalField.addTarget(self, action: "checkFields:", forControlEvents: .EditingChanged)
    frequencyField.addTarget(self, action: "checkFields:", forControlEvents: .EditingChanged)
    frequencyField.inputView = frequencyPicker
    if let habit = habitToEdit {
        title = "Edit Item"
        habitNameField.text = habit.name
        goalField.text = String(habit.numberLeft)
        doneBarButton.enabled = true
    }
}

override func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
    return nil
}

func textField(habitNameField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    let oldHabitNameText: NSString = habitNameField.text!
    let newHabitNameText: NSString = oldHabitNameText.stringByReplacingCharactersInRange(range, withString: string)
    doneBarButton.enabled = (newHabitNameText.length != 0)
    return true
}

func checkFields(sender: UITextField) {
    sender.text = sender.text?.stringByTrimmingCharactersInSet(.whitespaceCharacterSet())
    guard
        let habit = habitNameField.text where !habit.isEmpty,
        let goal = goalField.text where !goal.isEmpty,
        let frequency = frequencyField.text where !frequency.isEmpty
        else { return }
    // enable your button if all conditions are met
    doneBarButton.enabled = true
}
Derek Mei
  • 345
  • 1
  • 3
  • 9

11 Answers11

46

Xcode 9 • Swift 4

You can addTarget to your text fields to monitor for the control event .editingChanged and use a single selector method for all of them:

override func viewDidLoad() {
    super.viewDidLoad()
    doneBarButton.isEnabled = false
    [habitNameField, goalField, frequencyField].forEach({ $0.addTarget(self, action: #selector(editingChanged), for: .editingChanged) })
}

Create the selector method and use guard combined with where clause (Swift 3/4 uses a comma) to make sure all text fields are not empty otherwise just return. Swift 3 does not require @objc, but Swift 4 does:

@objc func editingChanged(_ textField: UITextField) {
    if textField.text?.characters.count == 1 {
        if textField.text?.characters.first == " " {
            textField.text = ""
            return
        }
    }
    guard
        let habit = habitNameField.text, !habit.isEmpty,
        let goal = goalField.text, !goal.isEmpty,
        let frequency = frequencyField.text, !frequency.isEmpty
    else {
        doneBarButton.isEnabled = false
        return
    }
    doneBarButton.isEnabled = true
}

sample

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • This is working, but for some reason, it works even if only of the fields is filled in. Why is that? – Derek Mei Jan 22 '16 at 07:48
  • Durr. That's a rookie mistake on my part. Thanks for helping me catch that. Also, the final text field for frequency isn't working because it's a picker view. Any ideas on that? – Derek Mei Jan 22 '16 at 07:58
  • You can create a boolean var to monitor it and flag it once the user sets it. Then just add it to the guard statement – Leo Dabus Jan 22 '16 at 07:59
  • how to check email id and not empty for single textfield, tried this but not working let emaiTextField = existingEmailAddressTextField.text, !emaiTextField.isEmpty, let validEmail = existingEmailAddressTextField.text, self.validateEmail(validEmail)==true, – karthikeyan Mar 16 '18 at 07:46
  • how to check valid email id or not for same text field – karthikeyan Mar 16 '18 at 09:32
14

Swift 5.1 /Xcode 11

enter image description here

override func viewDidLoad() {
    super.viewDidLoad()
    setupAddTargetIsNotEmptyTextFields()        
}

func setupAddTargetIsNotEmptyTextFields() {
    okButton.isHidden = true //hidden okButton
    nameUserTextField.addTarget(self, action: #selector(textFieldsIsNotEmpty), 
                                for: .editingChanged)
    emailUserTextField.addTarget(self, action: #selector(textFieldsIsNotEmpty), 
                                for: .editingChanged)
    passwordUserTextField.addTarget(self, action: #selector(textFieldsIsNotEmpty), 
                                for: .editingChanged)
    confimPasswordUserTextField.addTarget(self, action: #selector(textFieldsIsNotEmpty), 
                                for: .editingChanged)        
   }

and then create the selector method and use guard:

@objc func textFieldsIsNotEmpty(sender: UITextField) {

    sender.text = sender.text?.trimmingCharacters(in: .whitespaces)

    guard
      let name = nameUserTextField.text, !name.isEmpty,
      let email = emailUserTextField.text, !email.isEmpty,
      let password = passwordUserTextField.text, !password.isEmpty,
      let confirmPassword = confimPasswordUserTextField.text,
          password == confirmPassword          
      else
    {
      self.okButton.isHidden = true
      return
    }
    // enable okButton if all conditions are met
    okButton.isHidden = false
   }
A.Kant
  • 2,750
  • 3
  • 19
  • 15
  • Should be the correct answer, it works fine for me, the delegate method doesn't provide something like .editingChanged – Bradley Feb 03 '17 at 01:24
  • Great solution! You need to mark the 'textFieldsIsNotEmpty' as '@objc' else the compiler throws an error. Please update the answer. – Prabhav Nov 13 '17 at 06:25
  • how to check valid email id or not and empty for same text field – karthikeyan Mar 16 '18 at 09:33
4

The best way would be to Add observer in ViewDidLoad method. Than just check in textField Delegate method whether all TextFields are filled up or not. Once its filled up call oberserver method & in that you just need to enable button.

Note:

  • You can use observer for both enable or disable button

Hope it will help you.

Mayur Prajapati
  • 5,454
  • 7
  • 41
  • 70
3

I went ahead and abstracted this out a bit into a helper class that one can use for their swift project.

import Foundation
import UIKit

class ButtonValidationHelper {

  var textFields: [UITextField]!
  var buttons: [UIButton]!

  init(textFields: [UITextField], buttons: [UIButton]) {

    self.textFields = textFields
    self.buttons = buttons

    attachTargetsToTextFields()
    disableButtons()
    checkForEmptyFields()
  }

  //Attach editing changed listeners to all textfields passed in
  private func attachTargetsToTextFields() {
    for textfield in textFields{
        textfield.addTarget(self, action: #selector(textFieldsIsNotEmpty), for: .editingChanged)
    }
  }

  @objc private func textFieldsIsNotEmpty(sender: UITextField) {
    sender.text = sender.text?.trimmingCharacters(in: .whitespaces)
    checkForEmptyFields()
  }


  //Returns true if the field is empty, false if it not
  private func checkForEmptyFields() {

    for textField in textFields{
        guard let textFieldVar = textField.text, !textFieldVar.isEmpty else {
            disableButtons()
            return
        }
    }
    enableButtons()
  }

  private func enableButtons() {
    for button in buttons{
        button.isEnabled = true
    }
  }

  private func disableButtons() {
    for button in buttons{
        button.isEnabled = false
    }
  }

}

And then in your View Controller just simply init the helper with

buttonHelper = ButtonValidationHelper(textFields: [textfield1, textfield2, textfield3, textfield4], buttons: [button])

Make sure you keep a strong reference at top to prevent deallocation

var buttonHelper: ButtonValidationHelper!
Stus
  • 31
  • 2
1
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    if (textField == self.textField1) { /* do Something */ }
    else if (textField == self.textField2) { /* do Something */ }
    else if (textField == self.textField3) { /* do Something */ }

    // regardless of what you do before, doneBarButton is enabled when all are not empty
    doneBarButton.enabled = (textField1.length != 0) && (textField2.length != 0) && (textField3.length != 0)
    return true

}

Ramy Kfoury
  • 937
  • 5
  • 8
1

why not move the checking functionality to a separate function

func setDoneButtonStatus()
{
    let habitNameText: NSString = (habitNameField.text!).stringByReplacingCharactersInRange(range, withString: string)
    let goalText: NSString = (goalField.text!).stringByReplacingCharactersInRange(range, withString: string)
    let frequencyText: NSString = (frequencyField.text!).stringByReplacingCharactersInRange(range, withString: string)

    doneBarButton.enabled = (habitNameText.length != 0) && (goalText.length != 0) && (frequencyText.length != 0)
}

and then use

func textFieldShouldReturn(textField: UITextField) -> Bool
{
    textField.resignFirstResponder()
    setDoneButtonStatus()
}
Russell
  • 5,436
  • 2
  • 19
  • 27
1

This works for me: Hope it helps

  func textFieldDidEndEditing(textField: UITextField) {
        if txtField1.hasText() && textField2.hasText() && textField3.hasText(){
            doneBarButton.enabled = true
        }
    }
Alakh
  • 118
  • 8
  • Note that this is only available for iOS (10.0 and later), tvOS (10.0 and later). BTW it is not a method anymore. Now it is a computed property – Leo Dabus Apr 19 '17 at 15:46
1

You can create an array of text fields [UITextField] or an outlet collection. Let's call the array textFields or something like that.

doneBarButton.isEnabled = !textFields.flatMap { $0.text?.isEmpty }.contains(true)

And call the code above in a method that monitors text field's text changes.

nslllava
  • 589
  • 2
  • 9
  • 19
1

With Combine (Xcode11+, iOS13+) this became easier.

First you need to be able to create a publisher for text changes:

extension UITextField {

    var textPublisher: AnyPublisher<String, Never> {
        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: self)
            .compactMap { $0.object as? UITextField }
            .map { $0.text ?? "" }
            .eraseToAnyPublisher()
    }

}

Then you can combine multiple publishers from multiple text fields:

private var readyToLogIn: AnyPublisher<Bool, Never> {
   return Publishers
      .CombineLatest(
         emailTextField.textPublisher, passwordTextField.textPublisher
      )
      .map { email, password in
         !email.isEmpty && !password.isEmpty
      }
      .eraseToAnyPublisher()
}

And then change your button isEnabled property based on that combined Publisher

readyToLogIn
   .receive(on: RunLoop.main)
   .assign(to: \.isEnabled, on: signInButton)
Tiago Almeida
  • 14,081
  • 3
  • 67
  • 82
1

Swift 5.0+

I'd do something like this, which is a combination of answers already given, but more streamlined and up to date:

In your viewDidLoad()

    [usernameTextField, passwordTextField].forEach {
        $0?.addTarget(self,
                      action: #selector(editingChanged(_:)),
                      for: .editingChanged)
    }

Then create the below:

@objc func editingChanged(_ textField: UITextField) {
    
    // Trim whitespace and newlines
    textField.text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines)
    
    // Assumes loginButton exists and there's a reference to it in the current scope.
    loginButton.isEnabled = ![usernameTextField, passwordTextField].compactMap {
        $0.text?.isEmpty
    }.contains(true)
}
JaredH
  • 2,338
  • 1
  • 30
  • 40
0

Xcode 10.2 • Swift 4.3 version of Leo Dabus above.

This solution is for adding a user, probably the most common implementation for such validation.

override func viewDidLoad() {
super.viewDidLoad()
addUserButton.backgroundColor = disabledButtonColor
addUserButton.isEnabled = false

[emailField, userNameField, firstNameField, lastNameField].forEach { (field) in
    field?.addTarget(self,
                     action: #selector(editingChanged(_:)),
                     for: .editingChanged)
}}



@objc private func editingChanged(_ textField: UITextField) {
if textField.text?.count == 1 {
    if textField.text?.first == " " {
        textField.text = ""
        return
    }
}
guard
    let email       = emailField.text,      !email.isEmpty,
    let userName    = userNameField.text,   !userName.isEmpty,
    let firstName   = firstNameField.text,  !firstName.isEmpty,
    let lastName    = lastNameField.text,   !lastName.isEmpty
    else {
        addUserButton.isEnabled = false
        addUserButton.backgroundColor = disabledButtonColor
        return
}
addUserButton.isEnabled = true
addUserButton.backgroundColor = activeButtonColor

}

user550088
  • 712
  • 1
  • 8
  • 16