diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index 26df9645..0be691c8 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -141,6 +141,11 @@ EAB5FF0129424ACB00998C17 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAB5FF0029424ACB00998C17 /* UIControl.swift */; }; EABFEB642A26473700C4C106 /* NSAttributedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = EABFEB632A26473700C4C106 /* NSAttributedString.swift */; }; EAC58BFD2BE935C300BA39FA /* TitleLockupTextColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.swift */; }; + EAC58C062BED000200BA39FA /* CreditCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C052BED000200BA39FA /* CreditCard.swift */; }; + EAC58C082BED002D00BA39FA /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C072BED002D00BA39FA /* Date.swift */; }; + EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C092BED004E00BA39FA /* FieldType.swift */; }; + EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C0B2BED01D500BA39FA /* Telephone.swift */; }; + EAC58C0E2BED021600BA39FA /* Password.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC58C0D2BED021600BA39FA /* Password.swift */; }; EAC71A1D2A2E155A00E47A9F /* Checkbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */; }; EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */; }; EAC846F3294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */; }; @@ -337,6 +342,11 @@ EAB5FF0029424ACB00998C17 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; EABFEB632A26473700C4C106 /* NSAttributedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedString.swift; sourceTree = ""; }; EAC58BFC2BE935C300BA39FA /* TitleLockupTextColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleLockupTextColor.swift; sourceTree = ""; }; + EAC58C052BED000200BA39FA /* CreditCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCard.swift; sourceTree = ""; }; + EAC58C072BED002D00BA39FA /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; + EAC58C092BED004E00BA39FA /* FieldType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FieldType.swift; sourceTree = ""; }; + EAC58C0B2BED01D500BA39FA /* Telephone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Telephone.swift; sourceTree = ""; }; + EAC58C0D2BED021600BA39FA /* Password.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Password.swift; sourceTree = ""; }; EAC71A1C2A2E155A00E47A9F /* Checkbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Checkbox.swift; sourceTree = ""; }; EAC71A1E2A2E173D00E47A9F /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = ""; }; EAC846F2294B95CE00F685BA /* ButtonGroupCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGroupCollectionViewCell.swift; sourceTree = ""; }; @@ -875,6 +885,18 @@ path = Tooltip; sourceTree = ""; }; + EAC58C042BECFFEA00BA39FA /* FieldTypes */ = { + isa = PBXGroup; + children = ( + EAC58C092BED004E00BA39FA /* FieldType.swift */, + EAC58C052BED000200BA39FA /* CreditCard.swift */, + EAC58C072BED002D00BA39FA /* Date.swift */, + EAC58C0B2BED01D500BA39FA /* Telephone.swift */, + EAC58C0D2BED021600BA39FA /* Password.swift */, + ); + path = FieldTypes; + sourceTree = ""; + }; EAC9257E29119B5D00091998 /* TextLink */ = { isa = PBXGroup; children = ( @@ -907,6 +929,7 @@ EAC925862911C9DE00091998 /* InputField */ = { isa = PBXGroup; children = ( + EAC58C042BECFFEA00BA39FA /* FieldTypes */, EAC925872911C9DE00091998 /* InputField.swift */, EA2DC9B32BE2C6FE004F58C5 /* TextField.swift */, EA6642942BCEBF9500D81DC4 /* TextLinkModel.swift */, @@ -1142,6 +1165,7 @@ EA33622E2891EA3C0071C351 /* DispatchQueue+Once.swift in Sources */, EA4DB2FD28D3D0CA00103EE3 /* AnyEquatable.swift in Sources */, EA5E305A29510F8B0082B959 /* EnumSubset.swift in Sources */, + EAC58C082BED002D00BA39FA /* Date.swift in Sources */, EA985BF7296C665E00F2FF2E /* IconName.swift in Sources */, EA8141102A127066004F60D2 /* UIColor+VDSColor.swift in Sources */, EAF7F0AF289B144C00B287F5 /* UnderlineLabelAttribute.swift in Sources */, @@ -1195,12 +1219,14 @@ EA0D1C372A681CCE00E5C127 /* ToggleView.swift in Sources */, EAF7F0B9289C139800B287F5 /* ColorConfiguration.swift in Sources */, EA3361BD288B2C760071C351 /* TypeAlias.swift in Sources */, + EAC58C0A2BED004E00BA39FA /* FieldType.swift in Sources */, EA471F3A2A95587500CE9E58 /* LayoutConstraintable.swift in Sources */, EAB1D2CF28ABEF2B00DAE764 /* Typography+Base.swift in Sources */, EA0D1C3B2A6AD51B00E5C127 /* Typogprahy+Styles.swift in Sources */, EAF7F09A2899B17200B287F5 /* CATransaction.swift in Sources */, EA0D1C3D2A6AD57600E5C127 /* Typography+Enums.swift in Sources */, EAF1FE9B29DB1A6000101452 /* Changeable.swift in Sources */, + EAC58C0C2BED01D500BA39FA /* Telephone.swift in Sources */, EAF7F0A2289AFB3900B287F5 /* Errorable.swift in Sources */, EA8E40912A7D3F6300934ED3 /* UIView+Accessibility.swift in Sources */, EA6F330E2B911E9000BACAB9 /* TextView.swift in Sources */, @@ -1214,6 +1240,7 @@ 44604AD729CE196600E62B51 /* Line.swift in Sources */, 1808BEBC2BA41C3200129230 /* CarouselScrollbar.swift in Sources */, EAF978212A99035B00C2FEA9 /* Enabling.swift in Sources */, + EAC58C062BED000200BA39FA /* CreditCard.swift in Sources */, EA5E3058295105A40082B959 /* Tilelet.swift in Sources */, 186D13CB2BBA8B1500986B53 /* DropdownSelect.swift in Sources */, EA89201528B56CF4006B9984 /* RadioBoxGroup.swift in Sources */, @@ -1246,6 +1273,7 @@ EA985C692971B90B00F2FF2E /* IconSize.swift in Sources */, 71FC86E02B973AE500700965 /* DropShadowConfiguration.swift in Sources */, EA3362302891EB4A0071C351 /* Font.swift in Sources */, + EAC58C0E2BED021600BA39FA /* Password.swift in Sources */, EAF7F0AD289B142900B287F5 /* StrikeThroughLabelAttribute.swift in Sources */, EAB5FEF12927F4AA00998C17 /* SelfSizingCollectionView.swift in Sources */, EA3361B8288B2AAA0071C351 /* ViewProtocol.swift in Sources */, diff --git a/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift new file mode 100644 index 00000000..dc4aeaf2 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/CreditCard.swift @@ -0,0 +1,103 @@ +// +// CreditCard.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation +import UIKit + +extension InputField { + enum CreditCardType: CaseIterable { + case generic + case visa + case mastercard + case amex + case discover + case dinersClub + case jcb + case chinaUnionPay + + var image: UIImage { + return BundleManager.shared.image(for: imageName)! + } + + var imageName: String { + var imageName: String = "generic" + switch self { + case .visa: imageName = "visa" + case .mastercard: imageName = "mastercard" + case .amex: imageName = "amex" + case .discover: imageName = "discover" + case .dinersClub: imageName = "dinersClub" + case .jcb: imageName = "jcb" + default: imageName = "generic" + } + return imageName + } + + internal var separatorIndices: [Int] { + switch self { + case .dinersClub: + return [4, 10] + default: + return [4, 8, 12] + } + } + + internal var maxLength: Int { + switch self { + case .dinersClub: return 14 + default: return 16 + } + } + + static func from(iin: Int) -> CreditCardType? { + switch iin { + case 4000...4999: + return .visa + case 5100...5599, 2221...2720: + return .mastercard + case 3400...3499, 3700...3799: + return .amex + case 6011, 6221...6229, 6440...6499, 6500...6599: + return .discover + case 3600...3699, 3800...3999: + return .dinersClub + case 3528...3589: + return .jcb + case 6200...6299, 6000...6010, 8100...8199: + return .chinaUnionPay + default: + return nil + } + } + } + + 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 + } + 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 new file mode 100644 index 00000000..a8ef3d78 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Date.swift @@ -0,0 +1,65 @@ +// +// Date.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation + +extension InputField { + public enum DateFormat: String, CaseIterable { + case mmyy + case mmddyy + case mmddyyyy + + public var placeholderText: String { + switch self { + case .mmyy: "MM/YY" + case .mmddyy: "MM/DD/YY" + case .mmddyyyy: "MM/DD/YYYY" + } + } + + public var formatString: String { + switch self { + case .mmyy: "MM/yy" + case .mmddyy: "MM/dd/yy" + case .mmddyyyy: "MM/dd/yyyy" + } + } + + public var maxLength: Int { + switch self { + case .mmyy: 5 + case .mmddyy: 8 + case .mmddyyyy: 10 + } + } + + internal var separatorIndices: [Int] { + switch self { + case .mmyy: [2] + case .mmddyy: [2,4] + case .mmddyyyy: [2,4] + } + } + } + + 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.. PasswordAction { + self == .hide ? .show : .hide + } + } + +} diff --git a/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift new file mode 100644 index 00000000..3a13bc84 --- /dev/null +++ b/VDS/Components/TextFields/InputField/FieldTypes/Telephone.swift @@ -0,0 +1,43 @@ +// +// Tel.swift +// VDS +// +// Created by Matt Bruce on 5/9/24. +// + +import Foundation + +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 = creditCardMaxLength + $0.maxLength = creditCardType.maxLength $0.compareType = .equals $0.errorMessage = "Enter a valid credit card." } @@ -431,115 +423,9 @@ open class InputField: EntryFieldBase { } //-------------------------------------------------- - // MARK: - Password + // MARK: - Private Methods //-------------------------------------------------- - enum PasswordAction { - case show, hide - - func toggle() -> PasswordAction { - self == .hide ? .show : .hide - } - } - - internal var passwordActionType: PasswordAction = .show { didSet { setNeedsUpdate() } } - - open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } - open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } - - //-------------------------------------------------- - // MARK: - Date - //-------------------------------------------------- - open var dateFormat: DateFormat = .mmddyy { didSet { setNeedsUpdate() } } - - private 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.. String { - // Format the number in the style XXXX XXXX XXXX XXXX - var formattedNumber = "" - for (index, char) in number.enumerated() { - if index != 0 && index % 4 == 0 { - formattedNumber.append(" ") - } - formattedNumber.append(char) - } - - return formattedNumber - } - - private func updateCardTypeIcon(rawNumber: String) { -// let firstFourDigits = String(rawNumber.prefix(4)) -// if let icon = cardTypeIcons[firstFourDigits] { -// cardTypeIconView.image = icon -// } else { -// cardTypeIconView.image = nil -// } - } - - private func maskCreditCardNumber(_ number: String) -> String { - // Mask the first 12 characters if the length is 16 - let rawNumber = number.filter { $0.isNumber } - guard rawNumber.count == creditCardMaxLength else { return formatCreditCardNumber(number) } - let lastFourDigits = rawNumber.suffix(4) - let maskedSection = String(repeating: "•", count: 12) - let formattedMaskSection = formatCreditCardNumber(maskedSection) - return formattedMaskSection + " " + lastFourDigits - } - - //--------------------------------------------------- - // MARK: - Telephone - //--------------------------------------------------- - private 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.. UITextPosition? { + internal func cursorPosition(textField: UITextField, range: NSRange, replacementString string: String, rawNumber: String, formattedNumber: String) -> UITextPosition? { let start = range.location let length = string.count @@ -560,6 +446,28 @@ open class InputField: EntryFieldBase { return textField.position(from: textField.beginningOfDocument, offset: finalCursorLocation) } + //-------------------------------------------------- + // MARK: - Password + //-------------------------------------------------- + internal var passwordActionType: PasswordAction = .show { didSet { setNeedsUpdate() } } + open var hidePasswordButtonText: String = "Hide" { didSet { setNeedsUpdate() } } + open var showPasswordButtonText: String = "Show" { didSet { setNeedsUpdate() } } + + //-------------------------------------------------- + // MARK: - Date + //-------------------------------------------------- + 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 { @@ -596,7 +504,7 @@ extension InputField: UITextFieldDelegate { // Remove any existing formatting let rawNumber = newText.filter { $0.isNumber } - if rawNumber.count > creditCardMaxLength { + if rawNumber.count > creditCardType.maxLength { return false } @@ -606,11 +514,16 @@ extension InputField: UITextFieldDelegate { // 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 = getTelCursorPosition(textField: textField, + if let newPosition = cursorPosition(textField: textField, range: range, replacementString: string, rawNumber: rawNumber, @@ -670,7 +583,7 @@ extension InputField: UITextFieldDelegate { textField.text = formattedNumber // Calculate the new cursor position - if let newPosition = getTelCursorPosition(textField: textField, + if let newPosition = cursorPosition(textField: textField, range: range, replacementString: string, rawNumber: rawNumber, @@ -693,62 +606,21 @@ extension InputField: UITextFieldDelegate { } } -extension InputField.FieldType { +extension String { - public var keyboardType: UIKeyboardType { - switch self { - case .number: - .numberPad - case .tel: - .phonePad - case .creditCard: - .numberPad - case .date: - .numberPad - case .securityCode: - .numberPad - default: - .default - } - } -} - -extension InputField { - public enum DateFormat: String, CaseIterable { - case mmyy - case mmddyy - case mmddyyyy - - public var placeholderText: String { - switch self { - case .mmyy: "MM/YY" - case .mmddyy: "MM/DD/YY" - case .mmddyyyy: "MM/DD/YYYY" - } - } - - public var formatString: String { - switch self { - case .mmyy: "MM/yy" - case .mmddyy: "MM/dd/yy" - case .mmddyyyy: "MM/dd/yyyy" - } - } - - public var maxLength: Int { - switch self { - case .mmyy: 5 - case .mmddyy: 8 - case .mmddyyyy: 10 - } - } - - internal var separatorIndices: [Int] { - switch self { - case .mmyy: [2] - case .mmddyy: [2,4] - case .mmddyyyy: [2,4] - } + internal static func format(_ value: String, indices: [Int], with separator: String) -> String { + var formattedString = "" + var currentIndex = value.startIndex + + for index in 0..