refactored the rest of the inputfield code into the fieldtype handlers

Signed-off-by: Matt Bruce <matt.bruce@verizon.com>
This commit is contained in:
Matt Bruce 2024-05-09 10:03:56 -05:00
parent a656561073
commit 4ed2b3c894
10 changed files with 277 additions and 349 deletions

View File

@ -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
}
}
}

View File

@ -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..<formattedInput.count {
if dateFormat.separatorIndices.contains(index) {
formattedString.append("/")
}
formattedString.append(formattedInput[currentIndex])
currentIndex = formattedInput.index(after: currentIndex)
}
return formattedString
}
}

View File

@ -10,20 +10,7 @@ import UIKit
import VDSTokens
extension InputField {
protocol FieldTypeHandler: UITextFieldDelegate {
var keyboardType: UIKeyboardType { get }
var minWidth: CGFloat { get set }
var leftImageName: String? { get set }
var actionModel: InputField.TextLinkModel? { get set }
var toolTipModel: Tooltip.TooltipModel? { get set }
var isSecureTextEntry: Bool { get set }
var placeholderText: String? { get set }
func configure(for inputField: InputField)
func appendRules(for inputField: InputField)
}
class BaseFieldType: NSObject, FieldTypeHandler {
class FieldTypeHandler: NSObject {
var keyboardType: UIKeyboardType
var minWidth: CGFloat = 40.0
var leftImageName: String?
@ -31,13 +18,14 @@ extension InputField {
var toolTipModel: Tooltip.TooltipModel?
var isSecureTextEntry = false
var placeholderText: String?
var value: String?
internal override init() {
keyboardType = .default
super.init()
}
func configure(for inputField: InputField) {
func updateView(_ inputField: InputField) {
//textField
inputField.textField.isSecureTextEntry = isSecureTextEntry
@ -78,8 +66,18 @@ extension InputField {
inputField.tooltipModel = toolTipModel
}
func appendRules(for inputField: InputField) {}
func appendRules(_ inputField: InputField) {}
func textFieldDidBeginEditing(_ inputField: InputField, textField: UITextField) {
}
func textFieldDidEndEditing(_ inputField: InputField, textField: UITextField) {
}
func textField(_ inputField: InputField, textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
return true
}
}
public enum FieldType: String, CaseIterable {

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: centralOfficeCode)
}
if number.count > 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..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: lineNumber)
}
return formattedNumber
}
class TelephoneHandler: BaseFieldType {
class TelephoneHandler: FieldTypeHandler {
static let shared = TelephoneHandler()
private override init() {
@ -49,9 +18,13 @@ extension InputField {
self.keyboardType = .phonePad
}
override func configure(for inputField: InputField) {}
override func updateView(_ inputField: InputField) {
minWidth = 176.0
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 = "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..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: centralOfficeCode)
}
if number.count > 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..<endIndex]
formattedNumber.append("-")
formattedNumber.append(contentsOf: lineNumber)
}
return formattedNumber
}
}
}

View File

@ -9,15 +9,11 @@ import Foundation
import UIKit
extension InputField {
class TextHandler: BaseFieldType {
class TextHandler: FieldTypeHandler {
static let shared = TextHandler()
private override init() {
super.init()
}
override func configure(for inputField: InputField) {}
override func appendRules(for inputField: InputField) {}
}
}

View File

@ -86,8 +86,8 @@ open class InputField: EntryFieldBase {
/// Value for the textField
open override var value: String? {
if fieldType == .creditCard {
return creditCardRawNumber
if let value = fieldType.handler().value {
return value
} else {
return textField.text
}
@ -205,7 +205,7 @@ open class InputField: EntryFieldBase {
open override func updateView() {
//update fieldType first
updateFieldType()
fieldType.handler().updateView(self)
super.updateView()
@ -254,103 +254,10 @@ open class InputField: EntryFieldBase {
}
}
}
open func updateFieldType() {
fieldType.handler().configure(for: self)
var minWidth: CGFloat = 40.0
var leftImageName: String?
var actionModel: InputField.TextLinkModel?
var toolTipModel: Tooltip.TooltipModel? = tooltipModel
var isSecureTextEntry = false
var placeholderText: String?
switch fieldType {
case .text:
break
case .number:
break
case .inlineAction:
minWidth = 102.0
case .password:
let isHide = passwordActionType == .hide
let buttonText = isHide ?
hidePasswordButtonText.isEmpty ? "Hide" : hidePasswordButtonText :
showPasswordButtonText.isEmpty ? "Show" : showPasswordButtonText
isSecureTextEntry = !isHide
let nextPasswordActionType = passwordActionType.toggle()
if let text, !text.isEmpty {
actionModel = .init(text: buttonText,
onClick: { [weak self] _ in
guard let self else { return }
self.passwordActionType = nextPasswordActionType
})
} else {
passwordActionType = .show
}
minWidth = 62.0
case .creditCard:
minWidth = 288.0
leftImageName = creditCardType.imageName
case .telephone:
minWidth = 176.0
case .date:
minWidth = 114.0
placeholderText = dateFormat.placeholderText
case .securityCode:
minWidth = 88.0
isSecureTextEntry = true
}
//textField
textField.isSecureTextEntry = isSecureTextEntry
//leftIcon
if let leftImageName {
leftImageView.image = BundleManager.shared.image(for: leftImageName)?.withTintColor(iconColorConfiguration.getColor(self))
}
leftImageView.isHidden = leftImageName == nil
//actionLink
actionTextLink.surface = surface
if let actionModel {
actionTextLink.text = actionModel.text
actionTextLink.onClick = actionModel.onClick
actionTextLink.isHidden = false
containerStackView.setCustomSpacing(VDSLayout.space2X, after: statusIcon)
} else {
actionTextLink.isHidden = true
containerStackView.setCustomSpacing(0, after: statusIcon)
}
//set the width constraints
if let width, width > 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)
}
}