From 4ed2b3c8946746164fa7bb6762ba1b1cbad54a63 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Thu, 9 May 2024 10:03:56 -0500 Subject: [PATCH] refactored the rest of the inputfield code into the fieldtype handlers Signed-off-by: Matt Bruce --- .../InputField/FieldTypes/CreditCard.swift | 126 +++++++-- .../InputField/FieldTypes/Date.swift | 48 ++-- .../InputField/FieldTypes/FieldType.swift | 32 ++- .../InputField/FieldTypes/InlineAction.swift | 10 +- .../InputField/FieldTypes/Number.swift | 12 +- .../InputField/FieldTypes/Password.swift | 29 ++- .../InputField/FieldTypes/SecurityCode.swift | 16 +- .../InputField/FieldTypes/Telephone.swift | 105 +++++--- .../InputField/FieldTypes/Text.swift | 6 +- .../TextFields/InputField/InputField.swift | 242 +----------------- 10 files changed, 277 insertions(+), 349 deletions(-) diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift index 36eeb5f7..1eebba46 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -75,51 +75,121 @@ extension InputField { } } - class CreditCardHandler: BaseFieldType { + class CreditCardHandler: FieldTypeHandler { static let shared = CreditCardHandler() - + + var creditCardType: CreditCardType = .generic + private override init() { super.init() self.keyboardType = .numberPad } - override func configure(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + minWidth = 288.0 + leftImageName = creditCardType.imageName + + super.updateView(inputField) + } - override func appendRules(for inputField: InputField) { + override func appendRules(_ inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { - $0.maxLength = inputField.creditCardType.maxLength + $0.maxLength = creditCardType.maxLength $0.compareType = .equals $0.errorMessage = "Enter a valid credit card." } inputField.rules.append(.init(rule)) } } - } - - internal func formatCreditCardNumber(_ number: String) -> String { - let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes - return String.format(formattedInput, indices: creditCardType.separatorIndices, with: " ") - } - internal func updateCardTypeIcon(rawNumber: String) { - guard rawNumber.count >= 4, - let firstFourDigits = Int(String(rawNumber.prefix(4))), - let creditCardType = CreditCardType.from(iin: firstFourDigits) else { - leftImageView.image = BundleManager.shared.image(for: CreditCardType.generic.imageName) - creditCardType = .generic - return + override func textFieldDidBeginEditing(_ inputField: InputField, textField: UITextField) { + if let value { + textField.text = formatCreditCardNumber(value) + } } - self.creditCardType = creditCardType - } + + override func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) { + if let value { + textField.text = maskCreditCardNumber(value) + } + } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let allowedCharacters = CharacterSet.decimalDigits + if string.rangeOfCharacter(from: allowedCharacters.inverted) != nil && !string.isEmpty { + return false + } - internal func maskCreditCardNumber(_ number: String) -> String { - // Mask the first 12 characters if the length is 16 - let rawNumber = number.filter { $0.isNumber } - guard rawNumber.count == creditCardType.maxLength else { return formatCreditCardNumber(number) } - let lastFourDigits = rawNumber.suffix(4) - let maskedSection = String(repeating: "•", count: 12) - let formattedMaskSection = String.format(maskedSection, indices: creditCardType.separatorIndices, with: " ") - return formattedMaskSection + " " + lastFourDigits + // Get the current text + let currentText = textField.text ?? "" + + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + if rawNumber.count > creditCardType.maxLength { + return false + } + + // Format the number with spaces + let formattedNumber = formatCreditCardNumber(rawNumber) + + // Update the icon based on the first four digits + updateCardTypeIcon(inputField, rawNumber: rawNumber) + + // Check again + if rawNumber.count > creditCardType.maxLength { + return false + } + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = textField.cursorPosition(range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // if all passes, then set the number1 + value = rawNumber + + // Prevent the default behavior + return false + } + + /// Private + internal func formatCreditCardNumber(_ number: String) -> String { + let formattedInput = number.filter { $0.isNumber } // Remove any existing slashes + return String.format(formattedInput, indices: creditCardType.separatorIndices, with: " ") + } + + internal func updateCardTypeIcon(_ inputField: InputField, rawNumber: String) { + defer { inputField.setNeedsUpdate() } + + guard rawNumber.count >= 4, + let firstFourDigits = Int(String(rawNumber.prefix(4))), + let creditCardType = CreditCardType.from(iin: firstFourDigits) else { + creditCardType = .generic + return + } + + self.creditCardType = creditCardType + } + + internal func maskCreditCardNumber(_ number: String) -> String { + // Mask the first 12 characters if the length is 16 + let rawNumber = number.filter { $0.isNumber } + guard rawNumber.count == creditCardType.maxLength else { return formatCreditCardNumber(number) } + let lastFourDigits = rawNumber.suffix(4) + let maskedSection = String(repeating: "•", count: 12) + let formattedMaskSection = String.format(maskedSection, indices: creditCardType.separatorIndices, with: " ") + return formattedMaskSection + " " + lastFourDigits + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift index f8faf6dc..24f9fe59 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Date.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -10,7 +10,7 @@ import UIKit extension InputField { - class DateHandler: BaseFieldType { + class DateHandler: FieldTypeHandler { static let shared = DateHandler() private override init() { @@ -18,9 +18,14 @@ extension InputField { self.keyboardType = .numberPad } - override func configure(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + minWidth = 114.0 + placeholderText = inputField.dateFormat.placeholderText + + super.updateView(inputField) + } - override func appendRules(for inputField: InputField) { + override func appendRules(_ inputField: InputField) { if let text = inputField.textField.text, text.count > 0 { let rule = CharacterCountRule().copyWith { $0.maxLength = inputField.dateFormat.maxLength @@ -30,6 +35,27 @@ extension InputField { inputField.rules.append(.init(rule)) } } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + guard let oldText = textField.text, + let textRange = Range(range, in: oldText), + string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil else { + return false + } + + let newText = oldText.replacingCharacters(in: textRange, with: string) + if newText.count > inputField.dateFormat.maxLength { + return false + } + + if newText.count <= inputField.dateFormat.maxLength { + textField.text = String.format(newText, indices: inputField.dateFormat.separatorIndices, with: "/") + return false + } else { + return true + } + } } public enum DateFormat: String, CaseIterable { @@ -69,21 +95,5 @@ extension InputField { } } } - - internal func formatDate(_ input: String) -> String { - let formattedInput = input.filter { $0.isNumber } // Remove any existing slashes - var formattedString = "" - var currentIndex = formattedInput.startIndex - - for index in 0.. Bool { + return true + } + } public enum FieldType: String, CaseIterable { diff --git a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift index 8f4beac2..7bacecc0 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/InlineAction.swift @@ -10,16 +10,18 @@ import UIKit extension InputField { - class InlineActionHandler: BaseFieldType { + class InlineActionHandler: FieldTypeHandler { static let shared = InlineActionHandler() private override init() { super.init() } - override func configure(for inputField: InputField) {} - - override func appendRules(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + minWidth = 102.0 + + super.updateView(inputField) + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Number.swift b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift index 5961518e..9e653421 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Number.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Number.swift @@ -10,7 +10,7 @@ import UIKit extension InputField { - class NumberHandler: BaseFieldType { + class NumberHandler: FieldTypeHandler { static let shared = NumberHandler() private override init() { @@ -18,10 +18,12 @@ extension InputField { self.keyboardType = .numberPad } - override func configure(for inputField: InputField) {} - - override func appendRules(for inputField: InputField) {} - + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift index d1e7b112..892a8b9b 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Password.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Password.swift @@ -18,16 +18,37 @@ extension InputField { } } - class PasswordHandler: BaseFieldType { + class PasswordHandler: FieldTypeHandler { static let shared = PasswordHandler() + internal var passwordActionType: PasswordAction = .hide + private override init() { super.init() } - override func configure(for inputField: InputField) {} - - override func appendRules(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + let isHide = passwordActionType == .hide + let buttonText = isHide ? + inputField.hidePasswordButtonText.isEmpty ? "Hide" : inputField.hidePasswordButtonText : + inputField.showPasswordButtonText.isEmpty ? "Show" : inputField.showPasswordButtonText + + isSecureTextEntry = !isHide + let nextPasswordActionType = passwordActionType.toggle() + if let text = inputField.text, !text.isEmpty { + actionModel = .init(text: buttonText, + onClick: { [weak self] _ in + guard let self else { return } + self.passwordActionType = nextPasswordActionType + inputField.setNeedsUpdate() + }) + } else { + passwordActionType = .show + } + minWidth = 62.0 + + super.updateView(inputField) + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift index b7f7ed2a..f49800f4 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/SecurityCode.swift @@ -10,7 +10,7 @@ import UIKit extension InputField { - class SecurityCodeHandler: BaseFieldType { + class SecurityCodeHandler: FieldTypeHandler { static let shared = SecurityCodeHandler() private override init() { @@ -18,9 +18,19 @@ extension InputField { self.keyboardType = .numberPad } - override func configure(for inputField: InputField) {} + override func updateView(_ inputField: InputField) { + minWidth = 88.0 + isSecureTextEntry = true + + super.updateView(inputField) + } - override func appendRules(for inputField: InputField) {} + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + let allowedCharacters = CharacterSet.decimalDigits + let characterSet = CharacterSet(charactersIn: string) + return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= 4 + } } } diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift index 03dcc603..bfcd9ef7 100644 --- a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift +++ b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift @@ -10,38 +10,7 @@ import UIKit extension InputField { - internal func formatUSNumber(_ number: String) -> String { - // Format the number in the style XXX-XXX-XXXX - let areaCodeLength = 3 - let centralOfficeCodeLength = 3 - let lineNumberLength = 4 - - var formattedNumber = "" - - if number.count > 0 { - formattedNumber.append(contentsOf: number.prefix(areaCodeLength)) - } - - if number.count > areaCodeLength { - let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength) - let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength)) - let centralOfficeCode = number[startIndex.. areaCodeLength + centralOfficeCodeLength { - let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength) - let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength)) - let lineNumber = number[startIndex.. 0 { let rule = CharacterCountRule().copyWith { $0.maxLength = "XXX-XXX-XXXX".count @@ -61,6 +34,70 @@ extension InputField { inputField.rules.append(.init(rule)) } } + + override func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + // Allow only numbers and limit the length of text. + let allowedCharacters = CharacterSet(charactersIn: "01233456789") + let characterSet = CharacterSet(charactersIn: string) + let currentText = textField.text ?? "" + if !allowedCharacters.isSuperset(of: characterSet) { return false } + + // Calculate the new text + let newText = (currentText as NSString).replacingCharacters(in: range, with: string) + + // Remove any existing formatting + let rawNumber = newText.filter { $0.isNumber } + + // Format the number with dashes + let formattedNumber = formatUSNumber(rawNumber) + + // Set the formatted text + textField.text = formattedNumber + + // Calculate the new cursor position + if let newPosition = textField.cursorPosition(range: range, + replacementString: string, + rawNumber: rawNumber, + formattedNumber: formattedNumber) { + textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) + } + + // Prevent the default behavior + return false + + } + + internal func formatUSNumber(_ number: String) -> String { + // Format the number in the style XXX-XXX-XXXX + let areaCodeLength = 3 + let centralOfficeCodeLength = 3 + let lineNumberLength = 4 + + var formattedNumber = "" + + if number.count > 0 { + formattedNumber.append(contentsOf: number.prefix(areaCodeLength)) + } + + if number.count > areaCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(centralOfficeCodeLength, number.count - areaCodeLength)) + let centralOfficeCode = number[startIndex.. areaCodeLength + centralOfficeCodeLength { + let startIndex = number.index(number.startIndex, offsetBy: areaCodeLength + centralOfficeCodeLength) + let endIndex = number.index(startIndex, offsetBy: min(lineNumberLength, number.count - areaCodeLength - centralOfficeCodeLength)) + let lineNumber = number[startIndex.. minWidth { - widthConstraint?.constant = width - widthConstraint?.isActive = true - minWidthConstraint?.isActive = false - } else { - minWidthConstraint?.constant = minWidth - widthConstraint?.isActive = false - minWidthConstraint?.isActive = true - } - - //placeholder - textField.placeholder = placeholderText - - //tooltip - tooltipModel = toolTipModel - - } override func updateRules() { super.updateRules() - fieldType.handler().appendRules(for: self) + fieldType.handler().appendRules(self) } /// Used to update any Accessibility properties. @@ -390,10 +297,14 @@ open class InputField: EntryFieldBase { } return super.resignFirstResponder() } + + //-------------------------------------------------- + // MARK: - Public FieldType Properties + //-------------------------------------------------- + //-------------------------------------------------- // MARK: - Password //-------------------------------------------------- - internal var passwordActionType: PasswordAction = .show { didSet { setNeedsUpdate() } } open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } @@ -402,149 +313,20 @@ open class InputField: EntryFieldBase { //-------------------------------------------------- open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } - //--------------------------------------------------- - // MARK: - Credit Card - //--------------------------------------------------- - internal var creditCardRawNumber: String = "" - internal var creditCardType: CreditCardType = .generic { didSet { setNeedsUpdate() } } - - //--------------------------------------------------- - // MARK: - Telephone - //--------------------------------------------------- - } extension InputField: UITextFieldDelegate { public func textFieldDidBeginEditing(_ textField: UITextField) { - if fieldType == .creditCard { - textField.text = formatCreditCardNumber(creditCardRawNumber) - } + fieldType.handler().textFieldDidBeginEditing(self, textField: textField) } public func textFieldDidEndEditing(_ textField: UITextField) { - if self.fieldType == .creditCard { - textField.text = maskCreditCardNumber(creditCardRawNumber) - } + fieldType.handler().textFieldDidEndEditing(self, textField: textField) validate() } public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - -// case text, number, inlineAction, password, creditCard, tel, date, securityCode - - switch fieldType { - case .creditCard: - let allowedCharacters = CharacterSet.decimalDigits - if string.rangeOfCharacter(from: allowedCharacters.inverted) != nil && !string.isEmpty { - return false - } - - // Get the current text - let currentText = textField.text ?? "" - - // Calculate the new text - let newText = (currentText as NSString).replacingCharacters(in: range, with: string) - - // Remove any existing formatting - let rawNumber = newText.filter { $0.isNumber } - - if rawNumber.count > creditCardType.maxLength { - return false - } - - // Format the number with spaces - let formattedNumber = formatCreditCardNumber(rawNumber) - - // Update the icon based on the first four digits - updateCardTypeIcon(rawNumber: rawNumber) - - // Check again - if rawNumber.count > creditCardType.maxLength { - return false - } - - // Set the formatted text - textField.text = formattedNumber - - // Calculate the new cursor position - if let newPosition = textField.cursorPosition(range: range, - replacementString: string, - rawNumber: rawNumber, - formattedNumber: formattedNumber) { - textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) - } - - // if all passes, then set the number1 - creditCardRawNumber = rawNumber - - // Prevent the default behavior - return false - - case .date: - // Allow only numbers and limit the length of text. - guard let oldText = textField.text, - let textRange = Range(range, in: oldText), - string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil else { - return false - } - - let newText = oldText.replacingCharacters(in: textRange, with: string) - if newText.count > dateFormat.maxLength { - return false - } - - if newText.count <= dateFormat.maxLength { - textField.text = formatDate(newText) - return false - } else { - return true - } - - case .number: - // Allow only numbers - let allowedCharacters = CharacterSet.decimalDigits - let characterSet = CharacterSet(charactersIn: string) - return allowedCharacters.isSuperset(of: characterSet) - - case .telephone: - // Allow only numbers and limit the length of text. - let allowedCharacters = CharacterSet(charactersIn: "01233456789") - let characterSet = CharacterSet(charactersIn: string) - let currentText = textField.text ?? "" - if !allowedCharacters.isSuperset(of: characterSet) { return false } - - // Calculate the new text - let newText = (currentText as NSString).replacingCharacters(in: range, with: string) - - // Remove any existing formatting - let rawNumber = newText.filter { $0.isNumber } - - // Format the number with dashes - let formattedNumber = formatUSNumber(rawNumber) - - // Set the formatted text - textField.text = formattedNumber - - // Calculate the new cursor position - if let newPosition = textField.cursorPosition(range: range, - replacementString: string, - rawNumber: rawNumber, - formattedNumber: formattedNumber) { - textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) - } - - // Prevent the default behavior - return false - - case .securityCode: - // Allow only numbers and limit the length of text. - let allowedCharacters = CharacterSet.decimalDigits - let characterSet = CharacterSet(charactersIn: string) - return allowedCharacters.isSuperset(of: characterSet) && ((textField.text?.count ?? 0) + string.count - range.length) <= 4 - - default: - return true - } + return fieldType.handler().textField(self, textField: textField, shouldChangeCharactersIn: range, replacementString: string) } }