Skip to content

Instantly share code, notes, and snippets.

@JasonCanCode
Last active August 3, 2018 15:48
Show Gist options
  • Save JasonCanCode/d17f11cfdcc857e30e0b4ccfd3b336e0 to your computer and use it in GitHub Desktop.
Save JasonCanCode/d17f11cfdcc857e30e0b4ccfd3b336e0 to your computer and use it in GitHub Desktop.
A simpler way to handle multiple UITextFields
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
}
}
}
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