Last active
August 3, 2018 15:48
-
-
Save JasonCanCode/d17f11cfdcc857e30e0b4ccfd3b336e0 to your computer and use it in GitHub Desktop.
A simpler way to handle multiple UITextFields
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
protocol TextFieldKeyboardPresentable: class, UITextFieldDelegate { | |
/// Collection of all fields in a form in the order in which they should be navigated | |
var textFields: [UITextField] { get } | |
/// Can be used to take action once a form is completely filled out | |
func lastTextFieldDidReturn() | |
/// Can be used to enable a button when text field(s) become valid | |
func checkValidation() | |
} | |
extension TextFieldKeyboardPresentable where Self: UIViewController { | |
var currentResponder: UITextField? { | |
return textFields.lazy.filter({ $0.isFirstResponder }).first | |
} | |
/// For detemining whether to dismiss the keyboard | |
var lastTextField: UITextField { | |
guard let lastTextField = textFields.last else { | |
fatalError("Must create textFields computed property to make use of KeyboardPresentable") | |
} | |
return lastTextField | |
} | |
func updateViewOffset(forKeyboardFrame keyboardFrame: CGRect) { | |
let isKeyboardDismissing = keyboardFrame.origin.y >= UIScreen.main.bounds.height | |
guard let currentTextField = currentResponder, !isKeyboardDismissing else { | |
UIView.animate(withDuration: 0.2) { | |
self.view.frame.origin.y = 0.0 | |
} | |
return | |
} | |
let containingView: UIView = currentTextField.superview ?? self.view | |
let textFieldFrame = containingView.convert(currentTextField.frame, to: self.view) | |
let textFieldOffset: CGFloat = textFieldFrame.maxY + 3 | |
let accessoryHeight: CGFloat = currentTextField.inputAccessoryView?.frame.size.height ?? 0 | |
let heightOfKeyboard = keyboardFrame.size.height | |
let offsetTextFieldFromBottom: CGFloat = view.frame.size.height - (textFieldOffset + accessoryHeight) | |
UIView.animate(withDuration: 0.2) { | |
var frame = self.view.frame | |
frame.origin.y = min(0, offsetTextFieldFromBottom - heightOfKeyboard) | |
self.view.frame = frame | |
} | |
} | |
func dismissKeyboard() { | |
for textField in textFields where textField.isFirstResponder { | |
textField.resignFirstResponder() | |
return | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
/** EXAMPLE SUBCLASS: | |
class LoginViewController: TextFieldViewController { | |
@IBOutlet weak private var emailField: UITextField! | |
@IBOutlet weak private var passwordField: UITextField! | |
override var textFields: [UITextField] { | |
return [emailField, passwordField] | |
} | |
private var isFormValid: Bool { | |
return textPresent(in: textFields) | |
} | |
override func lastTextFieldDidReturn() { | |
super.lastTextFieldDidReturn() | |
if isFormValid { | |
login() | |
} | |
} | |
@IBAction private func loginButtonPressed(_ sender: UIButton) { | |
dismissKeyboard() | |
if isFormValid { | |
login() | |
} | |
} | |
} | |
*/ | |
class TextFieldViewController: UIViewController, TextFieldKeyboardPresentable { | |
var textFields: [UITextField] { | |
return [] | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(self.doDismissKeyboard (_:))) | |
self.view.addGestureRecognizer(tapGesture) | |
} | |
override func viewWillAppear(_ animated: Bool) { | |
super.viewWillAppear(animated) | |
NotificationCenter.default.addObserver(self, selector: #selector(gotKeyboardNotification), name: Notification.Name.UIKeyboardWillChangeFrame, object: nil) | |
for textField in textFields { | |
textField.addTarget(self, action: #selector(editingChanged), for: .editingChanged) | |
} | |
} | |
@objc private func editingChanged() { | |
checkValidation() | |
} | |
@objc func doDismissKeyboard (_ sender: UITapGestureRecognizer) { | |
dismissKeyboard() | |
} | |
override func viewWillDisappear(_ animated: Bool) { | |
super.viewWillDisappear(animated) | |
NotificationCenter.default.removeObserver(self, name: Notification.Name.UIKeyboardWillChangeFrame, object: nil) | |
} | |
@objc func gotKeyboardNotification(notification: NSNotification) { | |
guard let keyboardFrame: CGRect = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { | |
return | |
} | |
updateViewOffset(forKeyboardFrame: keyboardFrame) | |
} | |
func checkValidation() {} | |
/// Override to return `true` if you always want to resignFirstResponder and dismiss the keyboard. | |
func isLastTextField(_ textField: UITextField) -> Bool { | |
return textField == lastTextField | |
} | |
func lastTextFieldDidReturn() { | |
dismissKeyboard() | |
} | |
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { | |
checkValidation() | |
previousFieldButton.isEnabled = textField != textFields.first | |
nextFieldButton.isEnabled = !isLastTextField(textField) | |
return true | |
} | |
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { | |
textField.resignFirstResponder() | |
if textField.keyboardType == .phonePad { | |
formatPhoneNumber(in: textField) | |
} | |
return true | |
} | |
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { | |
let didReturn = string == "\n" | |
if didReturn && isLastTextField(textField) { | |
textField.resignFirstResponder() | |
return false | |
} | |
return true | |
} | |
func textFieldShouldReturn(_ textField: UITextField) -> Bool { | |
if isLastTextField(textField) { | |
textField.resignFirstResponder() | |
lastTextFieldDidReturn() | |
} else if let currentTextFieldIndex = textFields.index(of: textField) { | |
let nextTextField = textFields[currentTextFieldIndex + 1] | |
nextTextField.becomeFirstResponder() | |
} else { | |
textField.resignFirstResponder() | |
} | |
return true | |
} | |
func textPresent(in requiredFields: [UITextField]) -> Bool { | |
for textField in requiredFields where isValidTextPresent(in: textField.text) == false { | |
return false | |
} | |
return true | |
} | |
private func isValidTextPresent(in text: String?) -> Bool { | |
guard let text = text else { | |
return false | |
} | |
let trimmedText = text.trimmingCharacters(in: .whitespaces) | |
return !trimmedText.isEmpty | |
} | |
// MARK: - Phone Number | |
private func formatPhoneNumber(in textField: UITextField) { | |
let rawPhoneNumber = textField.text ?? "" | |
var numberText = extractNumbers(fromString: rawPhoneNumber) | |
if numberText.count >= 7 { | |
let strIndex = numberText.index(numberText.startIndex, offsetBy: 3) | |
numberText.insert("-", at: strIndex) | |
} | |
if numberText.count > 10 { | |
let strIndex = numberText.index(numberText.startIndex, offsetBy: 7) | |
numberText.insert("-", at: strIndex) | |
} | |
let maxCharCount = min(numberText.count, 12) | |
let strIndex = numberText.index(numberText.startIndex, offsetBy: maxCharCount) | |
textField.text = String(numberText[..<strIndex]) | |
} | |
private func extractNumbers(fromString str: String) -> String { | |
var numbers: String = "" | |
var remainingString = str | |
while let range = remainingString.range(of: "[0-9]{1,10}", options: .regularExpression) { | |
let numberSubstring = remainingString[range] | |
numbers += numberSubstring | |
remainingString.removeSubrange(range) | |
} | |
return numbers | |
} | |
// MARK: - Date Picker | |
lazy var datePicker: UIDatePicker = { | |
let datePicker = UIDatePicker(frame: CGRect(x: 0, y: 0, width: 350, height: 260)) | |
datePicker.datePickerMode = .date | |
datePicker.addTarget(self, action: #selector(datePickerChanged), for: .valueChanged) | |
return datePicker | |
}() | |
lazy var datePickerFormatter: DateFormatter = { | |
let dateFormatter = DateFormatter() | |
dateFormatter.dateStyle = .short | |
dateFormatter.timeStyle = .none | |
return dateFormatter | |
}() | |
@objc func datePickerChanged(datePicker: UIDatePicker) { | |
guard let textField = currentResponder else { | |
return | |
} | |
let strDate = datePickerFormatter.string(from: datePicker.date) | |
textField.text = strDate | |
} | |
// MARK: - Input Accessory | |
func useTextFieldInputAccessory() { | |
for textField in textFields { | |
textField.inputAccessoryView = customInputAccessory | |
} | |
} | |
lazy var customInputAccessory: UIView = { | |
let toolBar = UIToolbar() | |
toolBar.barStyle = .default | |
toolBar.isTranslucent = true | |
toolBar.tintColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1) | |
toolBar.backgroundColor = #colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 1) | |
toolBar.sizeToFit() | |
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) | |
toolBar.setItems([previousFieldButton, flexibleSpace, nextFieldButton], animated: false) | |
toolBar.isUserInteractionEnabled = true | |
return toolBar | |
}() | |
lazy var doneInputAccessory: UIView = { | |
let toolBar = UIToolbar() | |
toolBar.barStyle = .default | |
toolBar.isTranslucent = true | |
toolBar.tintColor = Theme.toolbarButtonBlue | |
toolBar.backgroundColor = Theme.toolbarBackgroundGray | |
toolBar.sizeToFit() | |
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) | |
toolBar.setItems([flexibleSpace, doneKeyboardButton], animated: false) | |
toolBar.isUserInteractionEnabled = true | |
return toolBar | |
}() | |
lazy var doneKeyboardButton: UIBarButtonItem = { | |
return UIBarButtonItem(title: Constants.Keyboard.accessoryDoneButtonText, | |
style: .plain, | |
target: self, | |
action: #selector(donePressed)) | |
}() | |
lazy var previousFieldButton: UIBarButtonItem = { | |
return UIBarButtonItem(title: Constants.Keyboard.accessoryPreviousButtonText, | |
style: .plain, | |
target: self, | |
action: #selector(previousPressed)) | |
}() | |
lazy var nextFieldButton: UIBarButtonItem = { | |
return UIBarButtonItem(title: Constants.Keyboard.accessoryNextButtonText, | |
style: .plain, | |
target: self, | |
action: #selector(nextPressed)) | |
}() | |
@objc func donePressed() { | |
guard let textField = currentResponder else { | |
return | |
} | |
textField.resignFirstResponder() | |
} | |
@objc func nextPressed() { | |
guard let textField = currentResponder, textField != lastTextField else { | |
return | |
} | |
if let currentTextFieldIndex = textFields.index(of: textField) { | |
let nextTextField = textFields[currentTextFieldIndex + 1] | |
nextTextField.becomeFirstResponder() | |
} else { | |
textField.resignFirstResponder() | |
} | |
} | |
@objc func previousPressed() { | |
guard let textField = currentResponder, textField != textFields.first else { | |
return | |
} | |
if let currentTextFieldIndex = textFields.index(of: textField) { | |
let nextTextField = textFields[currentTextFieldIndex - 1] | |
nextTextField.becomeFirstResponder() | |
} else { | |
textField.resignFirstResponder() | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment