├── .gitignore ├── .travis.yml ├── AnimatedCardInput.podspec ├── AnimatedCardInput ├── Assets │ ├── .gitkeep │ ├── american_express.png │ ├── diners_club.png │ ├── discover.png │ ├── jcb.png │ ├── mastercard.png │ └── visa.png └── Classes │ ├── .gitkeep │ ├── Card Inputs │ ├── CardInputField.swift │ └── CardInputsView.swift │ ├── Card View │ ├── CardBackSideView.swift │ ├── CardFrontSideView.swift │ ├── CardSideView.swift │ ├── CardView.swift │ └── CustomInputField.swift │ ├── Models │ ├── CardProvider.swift │ ├── CreditCardData.swift │ └── TextFieldType.swift │ └── Protocols │ ├── CardViewInputdelegate.swift │ ├── CreditCardDataDelegate.swift │ └── CreditCardDataProvider.swift ├── Example ├── AnimatedCardInput.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── AnimatedCardInput-Example.xcscheme ├── AnimatedCardInput.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── AnimatedCardInput │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── netguru.imageset │ │ │ ├── Contents.json │ │ │ └── netguru-logo-single-n.png │ ├── Info.plist │ └── ViewController.swift ├── Podfile ├── Podfile.lock ├── Pods │ ├── Local Podspecs │ │ └── AnimatedCardInput.podspec.json │ ├── Manifest.lock │ ├── Pods.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Target Support Files │ │ ├── AnimatedCardInput │ │ ├── AnimatedCardInput-Info.plist │ │ ├── AnimatedCardInput-dummy.m │ │ ├── AnimatedCardInput-prefix.pch │ │ ├── AnimatedCardInput-umbrella.h │ │ ├── AnimatedCardInput.debug.xcconfig │ │ ├── AnimatedCardInput.modulemap │ │ ├── AnimatedCardInput.release.xcconfig │ │ └── ResourceBundle-AnimatedCardInput-AnimatedCardInput-Info.plist │ │ ├── Pods-AnimatedCardInput_Example │ │ ├── Pods-AnimatedCardInput_Example-Info.plist │ │ ├── Pods-AnimatedCardInput_Example-acknowledgements.markdown │ │ ├── Pods-AnimatedCardInput_Example-acknowledgements.plist │ │ ├── Pods-AnimatedCardInput_Example-dummy.m │ │ ├── Pods-AnimatedCardInput_Example-frameworks.sh │ │ ├── Pods-AnimatedCardInput_Example-umbrella.h │ │ ├── Pods-AnimatedCardInput_Example.debug.xcconfig │ │ ├── Pods-AnimatedCardInput_Example.modulemap │ │ └── Pods-AnimatedCardInput_Example.release.xcconfig │ │ └── Pods-AnimatedCardInput_Tests │ │ ├── Pods-AnimatedCardInput_Tests-Info.plist │ │ ├── Pods-AnimatedCardInput_Tests-acknowledgements.markdown │ │ ├── Pods-AnimatedCardInput_Tests-acknowledgements.plist │ │ ├── Pods-AnimatedCardInput_Tests-dummy.m │ │ ├── Pods-AnimatedCardInput_Tests-umbrella.h │ │ ├── Pods-AnimatedCardInput_Tests.debug.xcconfig │ │ ├── Pods-AnimatedCardInput_Tests.modulemap │ │ └── Pods-AnimatedCardInput_Tests.release.xcconfig └── Tests │ ├── CardViewInputDelegateTests.swift │ ├── CreditCardDataDelegateTests.swift │ ├── CreditCardDataProviderTests.swift │ ├── CustomInputFieldTests.swift │ └── Info.plist ├── LICENSE ├── README.md └── _Pods.xcodeproj /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 26 | # Carthage/Checkouts 27 | 28 | Carthage/Build 29 | 30 | # We recommend against adding the Pods directory to your .gitignore. However 31 | # you should judge for yourself, the pros and cons are mentioned at: 32 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 33 | # 34 | # Note: if you ignore the Pods directory, make sure to uncomment 35 | # `pod install` in .travis.yml 36 | # 37 | # Pods/ 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * https://www.objc.io/issues/6-build-tools/travis-ci/ 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode7.3 6 | language: objective-c 7 | # cache: cocoapods 8 | # podfile: Example/Podfile 9 | # before_install: 10 | # - gem install cocoapods # Since Travis is not always on latest version 11 | # - pod install --project-directory=Example 12 | script: 13 | - set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/AnimatedCardInput.xcworkspace -scheme AnimatedCardInput-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty 14 | - pod lib lint 15 | -------------------------------------------------------------------------------- /AnimatedCardInput.podspec: -------------------------------------------------------------------------------- 1 | # Be sure to run `pod lib lint AnimatedCardInput.podspec' to ensure this is a valid spec before submitting. 2 | 3 | Pod::Spec.new do |s| 4 | s.name = 'AnimatedCardInput' 5 | s.version = '0.3.0' 6 | s.summary = 'iOS library with components for input of Credit Card data' 7 | 8 | s.description = "This library let's you drop into your project an animated component representing Credit Card and allow users to enter their data directly onto it." 9 | 10 | s.homepage = 'https://github.com/netguru/AnimatedCardInput' 11 | s.license = { :type => 'MIT', :file => 'LICENSE' } 12 | s.author = { 'Kamil Szczepański' => 'kamil.szczepanski@netguru.com' } 13 | s.source = { :git => 'https://github.com/netguru/AnimatedCardInput.git', :tag => s.version.to_s } 14 | s.social_media_url = 'https://www.netguru.com/codestories/topic/ios' 15 | 16 | s.swift_versions = '5.0' 17 | s.ios.deployment_target = '11.0' 18 | 19 | s.source_files = 'AnimatedCardInput/Classes/**/*' 20 | 21 | s.resource_bundles = { 22 | 'AnimatedCardInput' => ['AnimatedCardInput/Assets/*.png'] 23 | } 24 | end 25 | -------------------------------------------------------------------------------- /AnimatedCardInput/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/AnimatedCardInput/Assets/.gitkeep -------------------------------------------------------------------------------- /AnimatedCardInput/Assets/american_express.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/AnimatedCardInput/Assets/american_express.png -------------------------------------------------------------------------------- /AnimatedCardInput/Assets/diners_club.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/AnimatedCardInput/Assets/diners_club.png -------------------------------------------------------------------------------- /AnimatedCardInput/Assets/discover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/AnimatedCardInput/Assets/discover.png -------------------------------------------------------------------------------- /AnimatedCardInput/Assets/jcb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/AnimatedCardInput/Assets/jcb.png -------------------------------------------------------------------------------- /AnimatedCardInput/Assets/mastercard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/AnimatedCardInput/Assets/mastercard.png -------------------------------------------------------------------------------- /AnimatedCardInput/Assets/visa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/AnimatedCardInput/Assets/visa.png -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/AnimatedCardInput/Classes/.gitkeep -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Card Inputs/CardInputField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardInputField.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | 8 | /// View with title label and text field for input of credit card data. 9 | public final class CardInputField: UIView { 10 | 11 | // MARK: Properties 12 | 13 | /// Indicates if input should be checked for valid date value. 14 | internal var validatesDateInput: Bool = false { 15 | didSet { 16 | validateDate() 17 | } 18 | } 19 | 20 | /// Indicates maximum length of input. 21 | private let inputLimit: Int 22 | 23 | /// Indictes if input should be formatted as date. 24 | private let isDateInput: Bool 25 | 26 | /// Character used as date value separator. 27 | private let dateSeparator: String 28 | 29 | /// Date formatter used to validate date input. 30 | private lazy var dateFormatter: DateFormatter = { 31 | let formatter = DateFormatter() 32 | formatter.dateFormat = "MM\(dateSeparator)YY" 33 | return formatter 34 | }() 35 | 36 | // MARK: Hierarchy 37 | 38 | /// Label with title for Input Field. 39 | internal lazy var titleLabel: UILabel = { 40 | let label = UILabel() 41 | label.textColor = .black 42 | label.translatesAutoresizingMaskIntoConstraints = false 43 | label.font = UIFont.systemFont(ofSize: 12, weight: .light) 44 | return label 45 | }() 46 | 47 | private lazy var inputContainerView: UIView = { 48 | let view = UIView() 49 | view.translatesAutoresizingMaskIntoConstraints = false 50 | view.layer.cornerRadius = 8 51 | view.layer.borderWidth = 1 52 | view.layer.borderColor = UIColor.gray.cgColor 53 | view.backgroundColor = .clear 54 | return view 55 | }() 56 | 57 | /// Input Field. 58 | internal lazy var inputField: UITextField = { 59 | let textField = UITextField() 60 | textField.textColor = .black 61 | textField.translatesAutoresizingMaskIntoConstraints = false 62 | textField.font = UIFont.systemFont(ofSize: 24) 63 | textField.autocorrectionType = .no 64 | textField.delegate = self 65 | return textField 66 | }() 67 | 68 | // MARK: Initializers 69 | 70 | /// Initializes CardInputField. 71 | /// - Parameters: 72 | /// - title: Text to display as title above `Text Field`. 73 | /// - inputLimit: Maximum number of characters for this input. Defaults to 0 (unlimited) 74 | /// - isDateInput: Indictes if input should be formatted as date. Defaults to false. 75 | /// - dateSeparator: Character used as date separator. Defaults to `/` 76 | init( 77 | title: String, 78 | inputLimit: Int = 0, 79 | isDateInput: Bool = false, 80 | dateSeparator: String = "/" 81 | ) { 82 | self.inputLimit = inputLimit 83 | self.isDateInput = isDateInput 84 | self.dateSeparator = dateSeparator 85 | super.init(frame: .zero) 86 | 87 | titleLabel.text = title 88 | 89 | setupViewHierarchy() 90 | setupLayoutConstraints() 91 | setupProperties() 92 | setupBindings() 93 | } 94 | 95 | required init?(coder: NSCoder) { 96 | fatalError("init(coder:) has not been implemented") 97 | } 98 | 99 | // MARK: UITextFieldDelegate 100 | 101 | @discardableResult public override func becomeFirstResponder() -> Bool { 102 | inputField.becomeFirstResponder() 103 | } 104 | 105 | // MARK: Setup 106 | 107 | /// Setup Text Fields toolbar with custom buttons. 108 | /// - Parameters: 109 | /// - finishToolbarButton: button for finishing the editing. 110 | /// - previousToolbarButton: for returning to previous Text Field. 111 | /// - nextToolbarButton: button for progressing to next Text Field. 112 | internal func setupTextFieldToolbar( 113 | finishToolbarButton: UIBarButtonItem, 114 | previousToolbarButton: UIBarButtonItem, 115 | nextToolbarButton: UIBarButtonItem 116 | ) { 117 | inputField.inputAccessoryView = { 118 | let toolbar = UIToolbar() 119 | toolbar.items = [ 120 | finishToolbarButton, 121 | UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil), 122 | previousToolbarButton, 123 | nextToolbarButton, 124 | ] 125 | toolbar.sizeToFit() 126 | return toolbar 127 | }() 128 | } 129 | 130 | private func setupViewHierarchy() { 131 | inputContainerView.addSubview(inputField) 132 | [ 133 | titleLabel, 134 | inputContainerView, 135 | ].forEach(addSubview) 136 | } 137 | 138 | private func setupLayoutConstraints() { 139 | NSLayoutConstraint.activate([ 140 | titleLabel.topAnchor.constraint(equalTo: topAnchor), 141 | titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 6), 142 | titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -6), 143 | 144 | inputContainerView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4), 145 | inputContainerView.bottomAnchor.constraint(equalTo: bottomAnchor), 146 | inputContainerView.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor), 147 | inputContainerView.trailingAnchor.constraint(equalTo: titleLabel.trailingAnchor), 148 | 149 | inputField.topAnchor.constraint(equalTo: inputContainerView.topAnchor, constant: 6), 150 | inputField.bottomAnchor.constraint(equalTo: inputContainerView.bottomAnchor, constant: -6), 151 | inputField.leadingAnchor.constraint(equalTo: inputContainerView.leadingAnchor, constant: 6), 152 | inputField.trailingAnchor.constraint(equalTo: inputContainerView.trailingAnchor, constant: -6), 153 | ]) 154 | } 155 | 156 | private func setupProperties() { 157 | isUserInteractionEnabled = true 158 | } 159 | 160 | private func setupBindings() { 161 | inputField.addTarget(self, action: #selector(validateDate), for: .editingDidEnd) 162 | } 163 | 164 | // MARK: Private 165 | 166 | @objc private func validateDate() { 167 | guard 168 | validatesDateInput, 169 | let input = inputField.text, 170 | dateFormatter.date(from: input) == nil 171 | else { 172 | return 173 | } 174 | inputField.text = "" 175 | inputField.sendActions(for: .editingChanged) 176 | } 177 | } 178 | 179 | extension CardInputField: UITextFieldDelegate { 180 | 181 | public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 182 | let currentText = textField.text ?? "" 183 | guard let stringRange = Range(range, in: currentText) else { return false } 184 | let updatedText = currentText.replacingCharacters(in: stringRange, with: string) 185 | if 186 | isDateInput, 187 | currentText.count == 2, 188 | currentText.count < updatedText.count 189 | { 190 | textField.text = currentText + dateSeparator 191 | } 192 | return inputLimit == 0 || updatedText.count <= inputLimit 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Card Inputs/CardInputsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardInputsView.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | 8 | /// Scroll View containing `Card Input Fields` for input of Credit Card information. 9 | public class CardInputsView: UIScrollView { 10 | 11 | // MARK: Hierarchy 12 | 13 | /// Toolbar button for finishing the editing. 14 | internal var finishToolbarButton: UIBarButtonItem { 15 | UIBarButtonItem(title: "Finish", style: .plain, target: self, action: #selector(finishEditing)) 16 | } 17 | 18 | /// Toolbar button for progressing to next Text Field. 19 | internal var nextToolbarButton: UIBarButtonItem { 20 | UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(nextTextField)) 21 | } 22 | 23 | /// Toolbar button for returning to previous Text Field. 24 | internal var previousToolbarButton: UIBarButtonItem { 25 | UIBarButtonItem(title: "Previous", style: .plain, target: self, action: #selector(previousTextField)) 26 | } 27 | 28 | /// Container for Credit Card inputs. 29 | private lazy var stackView: UIStackView = { 30 | let stackView = UIStackView() 31 | stackView.translatesAutoresizingMaskIntoConstraints = false 32 | stackView.axis = .horizontal 33 | stackView.distribution = .fillEqually 34 | return stackView 35 | }() 36 | 37 | internal lazy var cardNumberField: CardInputField = { 38 | let textField = CardInputField(title: "Card Number", inputLimit: cardNumberDigitLimit) 39 | textField.inputField.keyboardType = .decimalPad 40 | textField.translatesAutoresizingMaskIntoConstraints = false 41 | let prevButton = self.previousToolbarButton 42 | prevButton.isEnabled = false 43 | textField.setupTextFieldToolbar( 44 | finishToolbarButton: self.finishToolbarButton, 45 | previousToolbarButton: prevButton, 46 | nextToolbarButton: self.nextToolbarButton 47 | ) 48 | return textField 49 | }() 50 | 51 | internal lazy var cardholderNameField: CardInputField = { 52 | let textField = CardInputField(title: "Cardholder Name") 53 | textField.translatesAutoresizingMaskIntoConstraints = false 54 | textField.setupTextFieldToolbar( 55 | finishToolbarButton: self.finishToolbarButton, 56 | previousToolbarButton: self.previousToolbarButton, 57 | nextToolbarButton: self.nextToolbarButton 58 | ) 59 | return textField 60 | }() 61 | 62 | internal lazy var validityDateField: CardInputField = { 63 | let textField = CardInputField(title: "Validity Date", inputLimit: 5, isDateInput: true) 64 | textField.validatesDateInput = true 65 | textField.inputField.keyboardType = .decimalPad 66 | textField.translatesAutoresizingMaskIntoConstraints = false 67 | textField.setupTextFieldToolbar( 68 | finishToolbarButton: self.finishToolbarButton, 69 | previousToolbarButton: self.previousToolbarButton, 70 | nextToolbarButton: self.nextToolbarButton 71 | ) 72 | return textField 73 | }() 74 | 75 | internal lazy var CVVNumberField: CardInputField = { 76 | let textField = CardInputField(title: "CVV Number", inputLimit: 3) 77 | textField.inputField.keyboardType = .decimalPad 78 | textField.translatesAutoresizingMaskIntoConstraints = false 79 | let nextButton = self.nextToolbarButton 80 | nextButton.isEnabled = false 81 | textField.setupTextFieldToolbar( 82 | finishToolbarButton: self.finishToolbarButton, 83 | previousToolbarButton: self.previousToolbarButton, 84 | nextToolbarButton: nextButton 85 | ) 86 | return textField 87 | }() 88 | 89 | // MARK: Properties 90 | 91 | /// - seeAlso: CardViewInputSelectionDelegate 92 | public weak var cardViewDelegate: CardViewInputDelegate? 93 | 94 | /// - seeAlso: CreditCardDataDelegate 95 | public weak var creditCardDataDelegate: CreditCardDataDelegate? 96 | 97 | /// Indicates if CVV Number should be masked. 98 | public var isSecureInput: Bool = false { 99 | willSet { 100 | CVVNumberField.inputField.isSecureTextEntry = newValue 101 | } 102 | } 103 | 104 | /// Indicates if Validity Date input is validated when retrieving data. 105 | public var validatesDateInput: Bool = true { 106 | willSet { 107 | validityDateField.validatesDateInput = newValue 108 | } 109 | } 110 | 111 | private let cardNumberDigitLimit: Int 112 | 113 | private let unfocusedFieldAlpha: CGFloat = 0.6 114 | 115 | private var shouldTextFieldsBecomeFirstResponder: Bool = true 116 | 117 | /// Card Provider for current input of Card Number. 118 | private var currentCardProvider: CardProvider? 119 | 120 | fileprivate var currentInput: TextFieldType = .cardNumber { 121 | didSet { 122 | switch currentInput { 123 | case .cardNumber: 124 | cardNumberField.alpha = 1 125 | cardholderNameField.alpha = unfocusedFieldAlpha 126 | if shouldTextFieldsBecomeFirstResponder { cardNumberField.becomeFirstResponder() } 127 | case .cardholderName: 128 | cardNumberField.alpha = unfocusedFieldAlpha 129 | cardholderNameField.alpha = 1 130 | validityDateField.alpha = unfocusedFieldAlpha 131 | if shouldTextFieldsBecomeFirstResponder { cardholderNameField.becomeFirstResponder() } 132 | case .validityDate: 133 | cardholderNameField.alpha = unfocusedFieldAlpha 134 | validityDateField.alpha = 1 135 | CVVNumberField.alpha = unfocusedFieldAlpha 136 | if shouldTextFieldsBecomeFirstResponder { validityDateField.becomeFirstResponder() } 137 | case .CVVNumber: 138 | validityDateField.alpha = unfocusedFieldAlpha 139 | CVVNumberField.alpha = 1 140 | if shouldTextFieldsBecomeFirstResponder { CVVNumberField.becomeFirstResponder() } 141 | case .none: 142 | break 143 | } 144 | } 145 | } 146 | 147 | // MARK: Configurable properties 148 | 149 | /// Color of text in title label. 150 | public var titleColor: UIColor? { 151 | get { cardNumberField.titleLabel.textColor } 152 | set(color) { updateTitleColor(color) } 153 | } 154 | 155 | /// Font of text in title label. 156 | public var titleFont: UIFont? { 157 | get { cardNumberField.titleLabel.font } 158 | set(font) { updateTitleFont(font) } 159 | } 160 | 161 | /// Color of text in text field. 162 | public var inputColor: UIColor? { 163 | get { cardNumberField.inputField.textColor } 164 | set(color) { updateInputColor(color) } 165 | } 166 | 167 | /// Font of text in text field. 168 | public var inputFont: UIFont? { 169 | get { cardNumberField.inputField.font } 170 | set(font) { updateInputFont(font) } 171 | } 172 | 173 | /// Color of tint for text field. 174 | public var inputTintColor: UIColor? { 175 | get { cardNumberField.inputField.tintColor } 176 | set(color) { updateTintColor(color) } 177 | } 178 | 179 | /// Color of border for text field. 180 | public var inputBorderColor: CGColor? { 181 | get { cardNumberField.inputField.layer.borderColor } 182 | set(color) { updateBorderColor(color) } 183 | } 184 | 185 | /// Custom string for title label of Card Number input. 186 | public var cardNumberTitle: String? { 187 | get { cardNumberField.titleLabel.text } 188 | set(text) { cardNumberField.titleLabel.text = text } 189 | } 190 | 191 | /// Custom string for title label of Cardholder Name input. 192 | public var cardholderNameTitle: String? { 193 | get { cardholderNameField.titleLabel.text } 194 | set(text) { cardholderNameField.titleLabel.text = text } 195 | } 196 | 197 | /// Custom string for title label of Validity Date input. 198 | public var validityDateTitle: String? { 199 | get { validityDateField.titleLabel.text } 200 | set(text) { validityDateField.titleLabel.text = text } 201 | } 202 | 203 | /// Custom string for title label of CVV Number input. 204 | public var cvvNumberTitle: String? { 205 | get { CVVNumberField.titleLabel.text } 206 | set(text) { CVVNumberField.titleLabel.text = text } 207 | } 208 | 209 | /// Character used as the Validity Date Separator - defaults to "/". 210 | public var validityDateSeparator: String = "/" 211 | 212 | // MARK: Initializers 213 | 214 | /// Initializes CardInputsView. 215 | /// Properties: 216 | /// - cardNumberDigitLimit: Limit of digits in credit card's number. Defaults to 16. 217 | public init( 218 | cardNumberDigitLimit: Int = 16 219 | ) { 220 | self.cardNumberDigitLimit = cardNumberDigitLimit 221 | super.init(frame: .zero) 222 | 223 | setupViewHierarchy() 224 | setupLayoutConstraints() 225 | setupProperties() 226 | setupTextFieldBindings() 227 | } 228 | 229 | public required init?(coder: NSCoder) { 230 | fatalError("init(coder:) has not been implemented") 231 | } 232 | 233 | // MARK: Setup 234 | 235 | private func setupViewHierarchy() { 236 | [ 237 | cardNumberField, 238 | cardholderNameField, 239 | validityDateField, 240 | CVVNumberField, 241 | ].forEach(stackView.addArrangedSubview) 242 | 243 | addSubview(stackView) 244 | } 245 | 246 | private func setupLayoutConstraints() { 247 | NSLayoutConstraint.activate([ 248 | stackView.heightAnchor.constraint(equalTo: heightAnchor), 249 | stackView.topAnchor.constraint(equalTo: topAnchor), 250 | stackView.bottomAnchor.constraint(equalTo: bottomAnchor), 251 | stackView.leadingAnchor.constraint(equalTo: leadingAnchor), 252 | stackView.trailingAnchor.constraint(equalTo: trailingAnchor), 253 | ]) 254 | 255 | [ 256 | cardNumberField, 257 | cardholderNameField, 258 | validityDateField, 259 | CVVNumberField, 260 | ].forEach { 261 | NSLayoutConstraint.activate([ 262 | $0.widthAnchor.constraint(equalTo: widthAnchor), 263 | $0.topAnchor.constraint(equalTo: stackView.topAnchor), 264 | $0.bottomAnchor.constraint(equalTo: stackView.bottomAnchor), 265 | ]) 266 | } 267 | } 268 | 269 | private func setupProperties() { 270 | translatesAutoresizingMaskIntoConstraints = false 271 | isPagingEnabled = true 272 | showsHorizontalScrollIndicator = false 273 | delegate = self 274 | clipsToBounds = false 275 | } 276 | 277 | private func setupTextFieldBindings() { 278 | cardNumberField.inputField.addTarget(self, action: #selector(cardNumberEditingChanged), for: .editingChanged) 279 | cardholderNameField.inputField.addTarget(self, action: #selector(cardholderNameEditingChanged), for: .editingChanged) 280 | validityDateField.inputField.addTarget(self, action: #selector(validityDateEditingChanged), for: .editingChanged) 281 | CVVNumberField.inputField.addTarget(self, action: #selector(CVVNumberEditingChanged), for: .editingChanged) 282 | 283 | cardNumberField.inputField.addTarget(self, action: #selector(cardNumberEditingBegan), for: .editingDidBegin) 284 | cardholderNameField.inputField.addTarget(self, action: #selector(cardholderNameEditingBegan), for: .editingDidBegin) 285 | validityDateField.inputField.addTarget(self, action: #selector(validityDateEditingBegan), for: .editingDidBegin) 286 | CVVNumberField.inputField.addTarget(self, action: #selector(CVVNumberEditingBegan), for: .editingDidBegin) 287 | } 288 | 289 | // MARK: Private 290 | 291 | private func updateTitleColor(_ color: UIColor?) { 292 | [ 293 | cardNumberField, 294 | cardholderNameField, 295 | validityDateField, 296 | CVVNumberField, 297 | ].forEach { $0.titleLabel.textColor = color } 298 | } 299 | 300 | private func updateTitleFont(_ font: UIFont?) { 301 | [ 302 | cardNumberField, 303 | cardholderNameField, 304 | validityDateField, 305 | CVVNumberField, 306 | ].forEach { $0.titleLabel.font = font } 307 | } 308 | 309 | private func updateInputColor(_ color: UIColor?) { 310 | [ 311 | cardNumberField, 312 | cardholderNameField, 313 | validityDateField, 314 | CVVNumberField, 315 | ].forEach { $0.inputField.textColor = color } 316 | } 317 | 318 | private func updateInputFont(_ font: UIFont?) { 319 | [ 320 | cardNumberField, 321 | cardholderNameField, 322 | validityDateField, 323 | CVVNumberField, 324 | ].forEach { $0.inputField.font = font } 325 | } 326 | 327 | private func updateTintColor(_ color: UIColor?) { 328 | [ 329 | cardNumberField, 330 | cardholderNameField, 331 | validityDateField, 332 | CVVNumberField, 333 | ].forEach { $0.inputField.tintColor = color } 334 | } 335 | 336 | private func updateBorderColor(_ color: CGColor?) { 337 | [ 338 | cardNumberField, 339 | cardholderNameField, 340 | validityDateField, 341 | CVVNumberField, 342 | ].forEach { $0.inputField.layer.borderColor = color } 343 | } 344 | 345 | private func updateCurrentInput(to type: TextFieldType) { 346 | guard type != currentInput else { return } 347 | currentInput = type 348 | } 349 | 350 | /// Informs Selection Delegate to progress to next Text Field. 351 | @objc private func nextTextField() { 352 | guard let nextInput = TextFieldType(rawValue: currentInput.rawValue + 1) else { return } 353 | updateCurrentInput(to: nextInput) 354 | } 355 | 356 | /// Informs Selection Delegate to return to previous Text Field. 357 | @objc private func previousTextField() { 358 | guard let prevInput = TextFieldType(rawValue: currentInput.rawValue - 1) else { return } 359 | updateCurrentInput(to: prevInput) 360 | } 361 | 362 | /// Informs Selection Delegate to finish editing. 363 | @objc internal func finishEditing() { 364 | endEditing(true) 365 | currentInput = .none 366 | } 367 | 368 | // MARK: Text Field Bindings 369 | 370 | @objc private func cardNumberEditingChanged() { 371 | guard let newValue = cardNumberField.inputField.text else { return } 372 | creditCardDataDelegate?.cardNumberChanged(newValue) 373 | currentCardProvider = CardProvider.recognizeProvider(from: newValue) 374 | } 375 | 376 | @objc private func cardholderNameEditingChanged() { 377 | guard let newValue = cardholderNameField.inputField.text else { return } 378 | creditCardDataDelegate?.cardholderNameChanged(newValue) 379 | } 380 | 381 | @objc private func validityDateEditingChanged() { 382 | guard let newValue = validityDateField.inputField.text else { return } 383 | creditCardDataDelegate?.validityDateChanged(newValue.replacingOccurrences(of: validityDateSeparator, with: "")) 384 | } 385 | 386 | @objc private func CVVNumberEditingChanged() { 387 | guard let newValue = CVVNumberField.inputField.text else { return } 388 | creditCardDataDelegate?.CVVNumberChanged(newValue) 389 | } 390 | 391 | @objc private func cardNumberEditingBegan() { 392 | currentInput = .cardNumber 393 | creditCardDataDelegate?.beganEditing(in: .cardNumber) 394 | } 395 | 396 | @objc private func cardholderNameEditingBegan() { 397 | currentInput = .cardholderName 398 | creditCardDataDelegate?.beganEditing(in: .cardholderName) 399 | } 400 | 401 | @objc private func validityDateEditingBegan() { 402 | currentInput = .validityDate 403 | creditCardDataDelegate?.beganEditing(in: .validityDate) 404 | } 405 | 406 | @objc private func CVVNumberEditingBegan() { 407 | currentInput = .CVVNumber 408 | creditCardDataDelegate?.beganEditing(in: .CVVNumber) 409 | } 410 | } 411 | 412 | // MARK: UIScrollViewDelegate 413 | 414 | extension CardInputsView: UIScrollViewDelegate { 415 | 416 | public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { 417 | let currentPage = Int(scrollView.contentOffset.x / scrollView.frame.size.width) 418 | guard let currentInput = TextFieldType(rawValue: currentPage) else { return } 419 | self.currentInput = currentInput 420 | } 421 | } 422 | 423 | // MARK: CreditCardDataDelegate 424 | 425 | extension CardInputsView: CreditCardDataDelegate { 426 | 427 | /// - SeeAlso: CreditCardDataDelegate.beganEditing(textFieldType:) 428 | public func beganEditing(in textFieldType: TextFieldType) { 429 | var pageFrame: CGRect = frame 430 | pageFrame.origin.x = pageFrame.size.width * CGFloat(textFieldType.rawValue) 431 | pageFrame.origin.y = 0 432 | scrollRectToVisible(pageFrame, animated: true) 433 | } 434 | 435 | /// - SeeAlso: CreditCardDataDelegate.cardNumberChanged(number:) 436 | public func cardNumberChanged(_ number: String) { 437 | cardNumberField.inputField.text = number 438 | } 439 | 440 | /// - SeeAlso: CreditCardDataDelegate.cardholderNameChanged(name:) 441 | public func cardholderNameChanged(_ name: String) { 442 | cardholderNameField.inputField.text = name 443 | } 444 | 445 | /// - SeeAlso: CreditCardDataDelegate.validityDateChanged(date:) 446 | public func validityDateChanged(_ date: String) { 447 | var formattedDate = date 448 | if formattedDate.count > 2 { 449 | formattedDate.insert(contentsOf: validityDateSeparator, at: formattedDate.index(formattedDate.startIndex, offsetBy: 2)) 450 | } 451 | validityDateField.inputField.text = formattedDate 452 | } 453 | 454 | /// - SeeAlso: CreditCardDataDelegate.CVVNumberChanged(cvv:) 455 | public func CVVNumberChanged(_ cvv: String) { 456 | CVVNumberField.inputField.text = cvv 457 | } 458 | } 459 | 460 | // MARK: CreditCardDataProvider 461 | 462 | extension CardInputsView: CreditCardDataProvider { 463 | 464 | /// - SeeAlso: CreditCardDataProvider.creditCardData 465 | public var creditCardData: CreditCardData { 466 | guard 467 | let cardNumber = cardNumberField.inputField.text, 468 | let cardholderName = cardholderNameField.inputField.text, 469 | let validityDate = validityDateField.inputField.text, 470 | let CVVNumber = CVVNumberField.inputField.text 471 | else { 472 | return CreditCardData() 473 | } 474 | return CreditCardData( 475 | cardProvider: currentCardProvider, 476 | cardNumber: cardNumber, 477 | cardholderName: cardholderName, 478 | validityDate: validityDate, 479 | CVVNumber: CVVNumber 480 | ) 481 | } 482 | } 483 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Card View/CardBackSideView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardBackSideView.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | 8 | /// View representing back side of a Credit Card. 9 | final class CardBackSideView: CardSideView { 10 | 11 | // MARK: Hierarchy 12 | 13 | /// Text Field for CVV Number input. 14 | internal lazy var CVVNumberField: CustomInputField = { 15 | let textField = CustomInputField(digitsLimit: CVVNumberDigitsLimit) 16 | textField.emptyCharacter = "X" 17 | textField.keyboardType = .decimalPad 18 | textField.backgroundColor = .white 19 | textField.font = UIFont.systemFont(ofSize: 20) 20 | 21 | let nextButton = nextToolbarButton 22 | nextButton.isEnabled = false 23 | textField.inputAccessoryView = { 24 | let toolbar = UIToolbar() 25 | toolbar.items = [finishToolbarButton, spacingToolbarItem, previousToolbarButton, nextButton] 26 | toolbar.sizeToFit() 27 | return toolbar 28 | }() 29 | return textField 30 | }() 31 | 32 | /// View imitating black bar at the back of Card. 33 | private lazy var blackBarView: UIView = { 34 | let view = UIView() 35 | view.translatesAutoresizingMaskIntoConstraints = false 36 | view.backgroundColor = .black 37 | return view 38 | }() 39 | 40 | /// Container imitating view for owner signature at the back of Card. 41 | private lazy var signatureBarsBackgroundStackView: UIStackView = { 42 | let stackView = UIStackView() 43 | stackView.translatesAutoresizingMaskIntoConstraints = false 44 | stackView.axis = .vertical 45 | stackView.distribution = .fillEqually 46 | for i in 0..<4 { 47 | let view = UIView() 48 | view.translatesAutoresizingMaskIntoConstraints = false 49 | view.backgroundColor = i % 2 == 0 ? .white : .gray 50 | stackView.addArrangedSubview(view) 51 | NSLayoutConstraint.activate([ 52 | view.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), 53 | view.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), 54 | ]) 55 | } 56 | return stackView 57 | }() 58 | 59 | // MARK: Properties 60 | 61 | /// Indicates maximum length of CVV Number Text Field. Defaults to 3. 62 | private let CVVNumberDigitsLimit: Int 63 | 64 | // MARK: Initializers 65 | 66 | /// Initializes Card Side View. 67 | /// - Parameters: 68 | /// - CVVNumberDigitsLimit: Indicates maximum length of CVV number. Defaults to 3. 69 | /// - seeAlso: CardSideView.init() 70 | init(CVVNumberDigitsLimit: Int = 3) { 71 | self.CVVNumberDigitsLimit = CVVNumberDigitsLimit 72 | super.init() 73 | 74 | setupViewHierarchy() 75 | setupLayoutConstraints() 76 | setupProperties() 77 | } 78 | 79 | required init?(coder: NSCoder) { 80 | fatalError("init(coder:) has not been implemented") 81 | } 82 | 83 | // MARK: Setup 84 | 85 | private func setupViewHierarchy() { 86 | [ 87 | blackBarView, 88 | signatureBarsBackgroundStackView, 89 | CVVNumberField, 90 | cardProviderView, 91 | ].forEach(addSubview) 92 | } 93 | 94 | private func setupLayoutConstraints() { 95 | NSLayoutConstraint.activate([ 96 | blackBarView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.2), 97 | blackBarView.topAnchor.constraint(equalTo: topAnchor, constant: 24), 98 | blackBarView.leadingAnchor.constraint(equalTo: leadingAnchor), 99 | blackBarView.trailingAnchor.constraint(equalTo: trailingAnchor), 100 | 101 | signatureBarsBackgroundStackView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.15), 102 | signatureBarsBackgroundStackView.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.5), 103 | signatureBarsBackgroundStackView.topAnchor.constraint(equalTo: blackBarView.bottomAnchor, constant: 24), 104 | signatureBarsBackgroundStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), 105 | 106 | CVVNumberField.heightAnchor.constraint(equalTo: signatureBarsBackgroundStackView.heightAnchor), 107 | CVVNumberField.widthAnchor.constraint(equalTo: signatureBarsBackgroundStackView.widthAnchor, multiplier: 0.3), 108 | CVVNumberField.leadingAnchor.constraint(equalTo: signatureBarsBackgroundStackView.trailingAnchor), 109 | CVVNumberField.centerYAnchor.constraint(equalTo: signatureBarsBackgroundStackView.centerYAnchor), 110 | 111 | cardProviderView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.15), 112 | cardProviderView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16), 113 | cardProviderView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor), 114 | cardProviderView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), 115 | ]) 116 | } 117 | 118 | /// - seeAlso: CardSideView.setupProperties() 119 | internal override func setupProperties() { 120 | super.setupProperties() 121 | 122 | isHidden = true 123 | transform = CGAffineTransform(scaleX: -1, y: 1) 124 | textColor = .black 125 | 126 | CVVNumberField.addTarget(self, action: #selector(selectedCVVField), for: .editingDidBegin) 127 | } 128 | 129 | /// - seeAlso: CardSideView.updateTextColor() 130 | internal override func updateTextColor() { 131 | CVVNumberField.textColor = textColor 132 | } 133 | 134 | // MARK: Private 135 | 136 | /// Updated current selection to CVV Number Field. 137 | @objc private func selectedCVVField() { 138 | inputDelegate?.updateCurrentInput(to: .CVVNumber) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Card View/CardFrontSideView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardFrontSideView.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | 8 | /// View representing front side of a Credit Card. 9 | final class CardFrontSideView: CardSideView { 10 | 11 | // MARK: Hierarchy 12 | 13 | /// Text Field for Card Number input. 14 | internal lazy var cardNumberField: CustomInputField = { 15 | let textField = CustomInputField( 16 | digitsLimit: cardNumberDigitsLimit, 17 | chunkLengths: cardNumberChunkLengths 18 | ) 19 | textField.separator = " " 20 | textField.emptyCharacter = "X" 21 | textField.font = .systemFont(ofSize: 24) 22 | textField.keyboardType = .decimalPad 23 | 24 | let prevButton = previousToolbarButton 25 | prevButton.isEnabled = false 26 | textField.inputAccessoryView = { 27 | let toolbar = UIToolbar() 28 | toolbar.items = [finishToolbarButton, spacingToolbarItem, prevButton, nextToolbarButton] 29 | toolbar.sizeToFit() 30 | return toolbar 31 | }() 32 | return textField 33 | }() 34 | 35 | /// Label with description of Cardholder Name field. 36 | internal lazy var cardholderNameLabel: UILabel = { 37 | let label = UILabel() 38 | label.translatesAutoresizingMaskIntoConstraints = false 39 | label.text = "Cardholder Name".uppercased() 40 | label.font = UIFont.systemFont(ofSize: 12, weight: .light) 41 | return label 42 | }() 43 | 44 | /// Text Field for Cordholder Name input. 45 | internal lazy var cardholderNameField: UITextField = { 46 | let textField = UITextField() 47 | textField.translatesAutoresizingMaskIntoConstraints = false 48 | textField.font = UIFont.systemFont(ofSize: 14) 49 | textField.attributedPlaceholder = NSAttributedString( 50 | string: "Name Surname".uppercased(), 51 | attributes: [ 52 | .foregroundColor: textColor?.withAlphaComponent(0.6) ?? .clear, 53 | .font: UIFont.systemFont(ofSize: 14), 54 | ] 55 | ) 56 | textField.tintColor = .clear 57 | textField.autocorrectionType = .no 58 | textField.inputAccessoryView = { 59 | let toolbar = UIToolbar() 60 | toolbar.items = [finishToolbarButton, spacingToolbarItem, previousToolbarButton, nextToolbarButton] 61 | toolbar.sizeToFit() 62 | return toolbar 63 | }() 64 | return textField 65 | }() 66 | 67 | /// Label with description of Validity Date Field. 68 | internal lazy var validityDateLabel: UILabel = { 69 | let label = UILabel() 70 | label.translatesAutoresizingMaskIntoConstraints = false 71 | label.text = "Valid Thru".uppercased() 72 | label.font = UIFont.systemFont(ofSize: 14, weight: .light) 73 | label.textAlignment = .right 74 | return label 75 | }() 76 | 77 | /// Text Field for Validity Date input. 78 | internal lazy var validityDateField: CustomInputField = { 79 | let textField = CustomInputField( 80 | digitsLimit: 4, 81 | chunkLengths: [2, 2] 82 | ) 83 | textField.validatesDateInput = true 84 | textField.separator = "/" 85 | textField.customPlaceholder = "MM/YY" 86 | textField.font = .systemFont(ofSize: 14) 87 | textField.keyboardType = .decimalPad 88 | textField.inputAccessoryView = { 89 | let toolbar = UIToolbar() 90 | toolbar.items = [finishToolbarButton, spacingToolbarItem, previousToolbarButton, nextToolbarButton] 91 | toolbar.sizeToFit() 92 | return toolbar 93 | }() 94 | 95 | return textField 96 | }() 97 | 98 | // MARK: Properties 99 | 100 | /// Indicates maximum length of Card Number Text Field. Defaults to 16. 101 | private let cardNumberDigitsLimit: Int 102 | 103 | /// Indicates format of card number, e.g. [4, 3] means that number of length 7 will be split 104 | /// into two parts of length 4 and 3 respectively (XXXX XXX). Defaults to [4, 4, 4, 4]. 105 | private let cardNumberChunkLengths: [Int] 106 | 107 | // MARK: Initializers 108 | 109 | /// Initializes Card Side View. 110 | /// - Parameters: 111 | /// - cardNumberDigitsLimit: Indicates maximum length of card number. Defaults to 16. 112 | /// - cardNumberChunkLengths: Indicates format of card number, 113 | /// e.g. [4, 3] means that number of length 7 will be split 114 | /// into two parts of length 4 and 3 respectively (XXXX XXX). 115 | /// - seeAlso: CardSideView.init() 116 | init( 117 | cardNumberDigitsLimit: Int = 16, 118 | cardNumberChunkLengths: [Int] = [4, 4, 4, 4] 119 | ) { 120 | self.cardNumberDigitsLimit = cardNumberDigitsLimit 121 | self.cardNumberChunkLengths = cardNumberChunkLengths 122 | super.init() 123 | 124 | setupViewHierarchy() 125 | setupLayoutConstraints() 126 | setupProperties() 127 | } 128 | 129 | required init?(coder: NSCoder) { 130 | fatalError("init(coder:) has not been implemented") 131 | } 132 | 133 | // MARK: Setup 134 | 135 | private func setupViewHierarchy() { 136 | [ 137 | cardProviderView, 138 | cardNumberField, 139 | cardholderNameLabel, 140 | cardholderNameField, 141 | validityDateLabel, 142 | validityDateField, 143 | ].forEach(addSubview) 144 | } 145 | 146 | private func setupLayoutConstraints() { 147 | NSLayoutConstraint.activate([ 148 | cardProviderView.heightAnchor.constraint(equalTo: heightAnchor, multiplier: 0.2), 149 | cardProviderView.topAnchor.constraint(equalTo: topAnchor, constant: 16), 150 | cardProviderView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor), 151 | cardProviderView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), 152 | 153 | cardNumberField.centerYAnchor.constraint(equalTo: centerYAnchor), 154 | cardNumberField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), 155 | cardNumberField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), 156 | 157 | cardholderNameLabel.centerYAnchor.constraint(equalTo: validityDateLabel.centerYAnchor), 158 | cardholderNameLabel.leadingAnchor.constraint(equalTo: cardholderNameField.leadingAnchor), 159 | 160 | cardholderNameField.heightAnchor.constraint(equalTo: validityDateField.heightAnchor), 161 | cardholderNameField.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -16), 162 | cardholderNameField.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16), 163 | 164 | validityDateLabel.bottomAnchor.constraint(equalTo: validityDateField.topAnchor, constant: -4), 165 | validityDateLabel.leadingAnchor.constraint(equalTo: cardholderNameLabel.trailingAnchor, constant: 16), 166 | validityDateLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), 167 | 168 | validityDateField.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 0.2), 169 | validityDateField.centerYAnchor.constraint(equalTo: cardholderNameField.centerYAnchor), 170 | validityDateField.leadingAnchor.constraint(greaterThanOrEqualTo: cardholderNameField.trailingAnchor, constant: 32), 171 | validityDateField.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16), 172 | ]) 173 | } 174 | 175 | /// - seeAlso: CardSideView.setupProperties() 176 | internal override func setupProperties() { 177 | super.setupProperties() 178 | 179 | textColor = .white 180 | 181 | cardNumberField.addTarget(self, action: #selector(updateCardProvider), for: .editingChanged) 182 | cardholderNameField.addTarget(self, action: #selector(capitalizeNameField), for: .editingChanged) 183 | 184 | cardNumberField.addTarget(self, action: #selector(selectedNumberField), for: .editingDidBegin) 185 | cardholderNameField.addTarget(self, action: #selector(selectedNameField), for: .editingDidBegin) 186 | validityDateField.addTarget(self, action: #selector(selectedValidityField), for: .editingDidBegin) 187 | } 188 | 189 | /// - seeAlso: CardSideView.updateTextColor() 190 | internal override func updateTextColor() { 191 | [ 192 | cardNumberField, 193 | cardholderNameField, 194 | validityDateField, 195 | ].forEach { 196 | $0.textColor = textColor 197 | } 198 | 199 | [ 200 | cardholderNameLabel, 201 | validityDateLabel, 202 | ].forEach { 203 | $0.textColor = textColor 204 | } 205 | 206 | cardholderNameField.attributedPlaceholder = NSAttributedString( 207 | string: (cardholderNameField.attributedPlaceholder?.string ?? "").uppercased(), 208 | attributes: [ 209 | .foregroundColor: textColor?.withAlphaComponent(0.5) ?? .clear, 210 | .font: cardholderNameField.font ?? UIFont.systemFont(ofSize: 14), 211 | ] 212 | ) 213 | } 214 | 215 | // MARK: Private 216 | 217 | /// Updates Cardholder Name Field input to uppercase. 218 | @objc private func capitalizeNameField() { 219 | cardholderNameField.text = cardholderNameField.text?.uppercased() 220 | cardholderNameField.sizeToFit() 221 | 222 | /// UITextField extends it's frame even when it is wrapping text inside, 223 | /// so we don't want to update indicator frame after reaching maximum width. 224 | guard validityDateField.frame.minX - cardholderNameField.frame.maxX >= 32 else { return } 225 | inputDelegate?.updateSelectionIndicator() 226 | } 227 | 228 | /// Updates Card Provider icon based on Card Number input. 229 | @objc private func updateCardProvider() { 230 | inputDelegate?.updateCardProvider(cardNumber: cardNumberField.text ?? "") 231 | } 232 | 233 | /// Updated current selection to Card Number Field. 234 | @objc private func selectedNumberField() { 235 | inputDelegate?.updateCurrentInput(to: .cardNumber) 236 | } 237 | 238 | /// Updated current selection to Cardholder Name Field. 239 | @objc private func selectedNameField() { 240 | inputDelegate?.updateCurrentInput(to: .cardholderName) 241 | } 242 | 243 | /// Updated current selection to Validity Date Field. 244 | @objc private func selectedValidityField() { 245 | inputDelegate?.updateCurrentInput(to: .validityDate) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Card View/CardSideView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardSideView.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | 8 | /// Base class used for implementation of front and back side views of a Credit Card. 9 | internal class CardSideView: UIView { 10 | 11 | // MARK: Hierarchy 12 | 13 | /// Toolbar button for finishing the editing. 14 | internal var finishToolbarButton: UIBarButtonItem { 15 | UIBarButtonItem(title: "Finish", style: .plain, target: self, action: #selector(finishEditing)) 16 | } 17 | 18 | /// Toolbar button for progressing to next Text Field. 19 | internal var nextToolbarButton: UIBarButtonItem { 20 | UIBarButtonItem(title: "Next", style: .plain, target: self, action: #selector(nextTextField)) 21 | } 22 | 23 | /// Toolbar button for returning to previous Text Field. 24 | internal var previousToolbarButton: UIBarButtonItem { 25 | UIBarButtonItem(title: "Previous", style: .plain, target: self, action: #selector(previousTextField)) 26 | } 27 | 28 | /// Toolbar item for flexible spacing. 29 | internal var spacingToolbarItem: UIBarButtonItem { 30 | UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) 31 | } 32 | 33 | /// Displays image for current card provider. 34 | internal lazy var cardProviderView: UIImageView = { 35 | let imageView = UIImageView() 36 | imageView.translatesAutoresizingMaskIntoConstraints = false 37 | imageView.contentMode = .scaleAspectFit 38 | imageView.alpha = 0 39 | return imageView 40 | }() 41 | 42 | /// Width constraint that ensures cardProviderIcon is properly aligned to right side after applying aspectFit scaling. 43 | private var cardProviderIconWidthConstraint: NSLayoutConstraint? 44 | 45 | // MARK: Properties 46 | 47 | /// - seeAlso: CardViewInputSelectionDelegate 48 | internal weak var inputDelegate: CardViewInputDelegate? 49 | 50 | /// Color of texts for Text Fields Labels on this Card Side. 51 | internal var textColor: UIColor? = .white { 52 | didSet { 53 | updateTextColor() 54 | } 55 | } 56 | 57 | /// Duration of showing/hiding `Card Provider Image View`. Defaults to 0.2. 58 | internal var cardProviderAnimationDuration: TimeInterval = 0.2 59 | 60 | /// Image for current card provider. 61 | internal var cardProviderImage: UIImage? { 62 | didSet { 63 | guard cardProviderImage != oldValue else { return } 64 | animateCardProviderIcon() 65 | } 66 | } 67 | 68 | // MARK: Initializers 69 | 70 | /// Initializes instance of CardSideView and setups basic properties. 71 | init() { 72 | super.init(frame: .zero) 73 | 74 | setupProperties() 75 | } 76 | 77 | required init?(coder aDecoder: NSCoder) { 78 | fatalError("init(coder:) has not been implemented") 79 | } 80 | 81 | // MARK: Setup 82 | 83 | /// Performs setup of basic properties with their default values. 84 | internal func setupProperties() { 85 | translatesAutoresizingMaskIntoConstraints = false 86 | backgroundColor = #colorLiteral(red: 0.2156862745, green: 0.2156862745, blue: 0.2156862745, alpha: 1) 87 | } 88 | 89 | /// Updates text color of all appropiate views. 90 | internal func updateTextColor() {} 91 | 92 | // MARK: Private 93 | 94 | /// Informs Selection Delegate to progress to next Text Field. 95 | @objc private func nextTextField() { 96 | inputDelegate?.moveToNextTextField() 97 | } 98 | 99 | /// Informs Selection Delegate to return to previous Text Field. 100 | @objc private func previousTextField() { 101 | inputDelegate?.moveToPreviousTextField() 102 | } 103 | 104 | /// Informs Selection Delegate to finish editing. 105 | @objc private func finishEditing() { 106 | inputDelegate?.finishEditing() 107 | } 108 | 109 | private func animateCardProviderIcon() { 110 | if let image = cardProviderImage { 111 | cardProviderIconWidthConstraint?.isActive = false 112 | cardProviderIconWidthConstraint = cardProviderView.widthAnchor.constraint(equalTo: cardProviderView.heightAnchor, multiplier: image.size.width / image.size.height) 113 | cardProviderIconWidthConstraint?.priority = .required 114 | cardProviderIconWidthConstraint?.isActive = true 115 | } 116 | 117 | let animator = UIViewPropertyAnimator(duration: cardProviderAnimationDuration, curve: .easeOut) 118 | if cardProviderView.alpha > 0 { 119 | animator.addAnimations { 120 | self.cardProviderView.alpha = 0 121 | } 122 | if cardProviderImage != nil { 123 | animator.addCompletion { _ in 124 | self.cardProviderView.image = self.cardProviderImage 125 | UIViewPropertyAnimator.runningPropertyAnimator( 126 | withDuration: self.cardProviderAnimationDuration, 127 | delay: 0, 128 | options: .curveEaseOut, 129 | animations: { 130 | self.cardProviderView.alpha = self.cardProviderImage == nil ? 0 : 1 131 | } 132 | ) 133 | } 134 | } 135 | } else { 136 | cardProviderView.image = cardProviderImage 137 | animator.addAnimations { 138 | self.cardProviderView.alpha = self.cardProviderImage == nil ? 0 : 1 139 | } 140 | } 141 | animator.addCompletion { _ in 142 | if self.cardProviderImage == nil { 143 | self.cardProviderView.image = nil 144 | } 145 | } 146 | animator.startAnimation() 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Card View/CardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardView.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | 8 | /// View acting as a container for front and back side of the Credit Card. 9 | public final class CardView: UIView { 10 | 11 | // MARK: Hierarchy 12 | 13 | /// Height constraint that allows to keep Credit Card view in standard aspect. 14 | private lazy var heightConstraint: NSLayoutConstraint = { 15 | let constraint = heightAnchor.constraint(equalTo: widthAnchor, multiplier: 53.98 / 85.60) 16 | constraint.priority = .required 17 | return constraint 18 | }() 19 | 20 | /// Container for front side of Credit Card. 21 | private lazy var cardContainer: UIView = { 22 | let view = UIView() 23 | view.translatesAutoresizingMaskIntoConstraints = false 24 | return view 25 | }() 26 | 27 | /// Container for front side of Credit Card. 28 | private lazy var frontSideContainer: CardFrontSideView = { 29 | let view = CardFrontSideView( 30 | cardNumberDigitsLimit: cardNumberDigitsLimit, 31 | cardNumberChunkLengths: cardNumberChunkLengths 32 | ) 33 | view.inputDelegate = self 34 | return view 35 | }() 36 | 37 | /// Container for back side of Credit Card. 38 | private lazy var backSideContainer: CardBackSideView = { 39 | let view = CardBackSideView( 40 | CVVNumberDigitsLimit: CVVNumberDigitsLimit 41 | ) 42 | view.inputDelegate = self 43 | return view 44 | }() 45 | 46 | /// View that transition to currently selected Text Field. 47 | private lazy var selectionIndicator: UIView = { 48 | let view = UIView(frame: .zero) 49 | view.translatesAutoresizingMaskIntoConstraints = false 50 | view.backgroundColor = .clear 51 | view.isHidden = true 52 | view.layer.borderWidth = 1 53 | view.layer.borderColor = UIColor.orange.cgColor 54 | view.layer.cornerRadius = 8 55 | return view 56 | }() 57 | 58 | /// Handles taps outside of Text Fields to finish editing. 59 | private let tapGestureRecognizer = UITapGestureRecognizer() 60 | 61 | // MARK: Properties 62 | 63 | /// - seeAlso: CreditCardDataDelegate 64 | public weak var creditCardDataDelegate: CreditCardDataDelegate? 65 | 66 | /// Card Provider for current input of Card Number. 67 | private var currentCardProvider: CardProvider? { 68 | willSet { 69 | cardProviderImage = newValue?.icon 70 | } 71 | } 72 | 73 | private var shouldTextFieldsBecomeFirstResponder: Bool = true 74 | 75 | /// Indicates which Text Field is currently selected. 76 | public var currentInput: TextFieldType = .cardNumber { 77 | didSet { 78 | if shouldTextFieldsBecomeFirstResponder { 79 | switch currentInput { 80 | case .cardNumber: 81 | frontSideContainer.cardNumberField.becomeFirstResponder() 82 | case .cardholderName: 83 | frontSideContainer.cardholderNameField.becomeFirstResponder() 84 | case .validityDate: 85 | frontSideContainer.validityDateField.becomeFirstResponder() 86 | case .CVVNumber: 87 | backSideContainer.CVVNumberField.becomeFirstResponder() 88 | case .none: 89 | break 90 | } 91 | } 92 | if (currentInput != oldValue) && (currentInput == .CVVNumber && backSideContainer.isHidden) || (currentInput != .CVVNumber && frontSideContainer.isHidden) { 93 | flip() 94 | } else { 95 | updateIndicator(animated: true) 96 | } 97 | } 98 | } 99 | 100 | // MARK: Configurable properties 101 | 102 | /// Indicates if CVV Number should be masked. 103 | public var isSecureInput: Bool = false { 104 | willSet { 105 | backSideContainer.CVVNumberField.isSecureMode = newValue 106 | } 107 | } 108 | 109 | /// Indicates if Validity Date input is validated when retrieving data. 110 | public var validatesDateInput: Bool = true { 111 | willSet { 112 | frontSideContainer.validityDateField.validatesDateInput = newValue 113 | } 114 | } 115 | 116 | /// Image for the current card's provider. 117 | public var cardProviderImage: UIImage? { 118 | get { frontSideContainer.cardProviderImage } 119 | set(image) { 120 | frontSideContainer.cardProviderImage = image 121 | backSideContainer.cardProviderImage = image 122 | } 123 | } 124 | 125 | /// Background color of the card's front side - defaults to #373737. 126 | public var frontSideCardColor: UIColor? { 127 | get { frontSideContainer.backgroundColor } 128 | set(color) { frontSideContainer.backgroundColor = color } 129 | } 130 | 131 | /// Text color of the card's front side - defaults to #FFFFFF. 132 | public var frontSideTextColor: UIColor? { 133 | get { frontSideContainer.textColor } 134 | set(color) { frontSideContainer.textColor = color } 135 | } 136 | 137 | /// Background color of the card's back side - defaults to #373737. 138 | public var backSideCardColor: UIColor? { 139 | get { backSideContainer.backgroundColor } 140 | set(color) { backSideContainer.backgroundColor = color } 141 | } 142 | 143 | /// Background color of the card's CVV Field - defaults to #FFFFFF. 144 | public var CVVBackgroundColor: UIColor? { 145 | get { backSideContainer.CVVNumberField.backgroundColor } 146 | set(color) { backSideContainer.CVVNumberField.backgroundColor = color } 147 | } 148 | 149 | /// Text color of the card's back side - defaults to #000000. 150 | public var backSideTextColor: UIColor? { 151 | get { backSideContainer.textColor } 152 | set(color) { backSideContainer.textColor = color } 153 | } 154 | 155 | /// Border color of a selected field indicator - defaults to #ff8000. 156 | public var selectionIndicatorColor: UIColor? { 157 | get { UIColor(cgColor: selectionIndicator.layer.borderColor ?? UIColor.clear.cgColor) } 158 | set(color) { selectionIndicator.layer.borderColor = color?.cgColor } 159 | } 160 | 161 | /// Font of the Card Number Field - defaults to System SemiBold 24. 162 | public var numberInputFont: UIFont? { 163 | get { frontSideContainer.cardNumberField.font } 164 | set(font) { frontSideContainer.cardNumberField.font = font } 165 | } 166 | 167 | /// Font of the Cardholder Name Label - defaults to System Light 14. 168 | /// Recommended font size is 0.6 of Card Number size. 169 | public var nameLabelFont: UIFont? { 170 | get { frontSideContainer.cardholderNameLabel.font } 171 | set(font) { frontSideContainer.cardholderNameLabel.font = font } 172 | } 173 | 174 | /// Font of the Cardholder Name Field - defaults to System Regular 14. 175 | /// Recommended font size is 0.6 of Card Number size. 176 | public var nameInputFont: UIFont? { 177 | get { frontSideContainer.cardholderNameField.font } 178 | set(font) { frontSideContainer.cardholderNameField.font = font } 179 | } 180 | 181 | /// Font of the Validity Date Label - defaults to System Light 14. 182 | /// Recommended font size is 0.6 of Card Number size. 183 | public var validityLabelFont: UIFont? { 184 | get { frontSideContainer.cardholderNameLabel.font } 185 | set(font) { frontSideContainer.cardholderNameLabel.font = font } 186 | } 187 | 188 | /// Font of the Validity Date Field - defaults to System Regular 14. 189 | /// Recommended font size is 0.6 of Card Number size. 190 | public var validityInputFont: UIFont? { 191 | get { frontSideContainer.validityDateField.font } 192 | set(font) { frontSideContainer.validityDateField.font = font } 193 | } 194 | 195 | /// Font of the CVV Number Field - defaults to System SemiBold 20. 196 | /// Recommended font size is 0.85 of Card Number size. 197 | public var CVVInputFont: UIFont? { 198 | get { backSideContainer.CVVNumberField.font } 199 | set(font) { backSideContainer.CVVNumberField.font = font } 200 | } 201 | 202 | /// Character used as the Card Number Separator - defaults to " ". 203 | public var cardNumberSeparator: String { 204 | get { frontSideContainer.cardNumberField.separator } 205 | set(separator) { frontSideContainer.cardNumberField.separator = separator } 206 | } 207 | 208 | /// Character used as the Card Number Empty Character - defaults to "X". 209 | public var cardNumberEmptyCharacter: String { 210 | get { frontSideContainer.cardNumberField.emptyCharacter } 211 | set(emptyCharacter) { frontSideContainer.cardNumberField.emptyCharacter = emptyCharacter } 212 | } 213 | 214 | /// Character used as the Validity Date Separator - defaults to "/". 215 | public var validityDateSeparator: String { 216 | get { frontSideContainer.validityDateField.separator } 217 | set(separator) { frontSideContainer.validityDateField.separator = separator } 218 | } 219 | 220 | /// Text used as the Validity Date Placeholder - defaults to "MM/YY". 221 | public var validityDateCustomPlaceHolder: String { 222 | get { frontSideContainer.validityDateField.emptyCharacter } 223 | set(customPlaceholder) { frontSideContainer.validityDateField.customPlaceholder = customPlaceholder } 224 | } 225 | 226 | /// Character used as CVV Number Empty Character - defaults to "X". 227 | public var CVVNumberEmptyCharacter: String { 228 | get { backSideContainer.CVVNumberField.emptyCharacter } 229 | set(emptyCharacter) { backSideContainer.CVVNumberField.emptyCharacter = emptyCharacter } 230 | } 231 | 232 | /// Custom string for title label of Cardholder Name input. 233 | public var cardholderNameTitle: String? { 234 | get { frontSideContainer.cardholderNameLabel.text } 235 | set(text) { frontSideContainer.cardholderNameLabel.text = text } 236 | } 237 | 238 | /// Custom string for placeholder of Cardholder Name input. 239 | public var cardholderNamePlaceholder: String? { 240 | get { frontSideContainer.cardholderNameField.placeholder } 241 | set(text) { frontSideContainer.cardholderNameField.placeholder = text } 242 | } 243 | 244 | /// Custom string for title label of Validity Date input. 245 | public var validityDateTitle: String? { 246 | get { frontSideContainer.validityDateLabel.text } 247 | set(text) { frontSideContainer.validityDateLabel.text = text } 248 | } 249 | 250 | /// Indicates maximum length of Card Number Text Field. 251 | private let cardNumberDigitsLimit: Int 252 | 253 | /// Indicates format of card number, e.g. [4, 3] means that number of length 7 will be split 254 | /// into two parts of length 4 and 3 respectively (XXXX XXX). Defaults to [4, 4, 4, 4]. 255 | private let cardNumberChunkLengths: [Int] 256 | 257 | /// Indicates maximum length of CVV Number Text Field. Defaults to 3. 258 | private let CVVNumberDigitsLimit: Int 259 | 260 | // MARK: Initializers 261 | 262 | /// Initializes Card View. 263 | /// - Parameters: 264 | /// - cardNumberDigitsLimit: Indicates maximum length of card number. Defaults to 16. 265 | /// - cardNumberChunkLengths: Indicates format of card number, 266 | /// e.g. [4, 3] means that number of length 7 will be split 267 | /// into two parts of length 4 and 3 respectively (XXXX XXX). 268 | /// - CVVNumberDigitsLimit: Indicates maximum length of CVV number. Defaults to 3. 269 | public init( 270 | cardNumberDigitsLimit: Int = 16, 271 | cardNumberChunkLengths: [Int] = [4, 4, 4, 4], 272 | CVVNumberDigitsLimit: Int = 3 273 | ) { 274 | self.cardNumberDigitsLimit = cardNumberDigitsLimit 275 | self.cardNumberChunkLengths = cardNumberChunkLengths 276 | self.CVVNumberDigitsLimit = CVVNumberDigitsLimit 277 | 278 | super.init(frame: .zero) 279 | 280 | setupViewHierarchy() 281 | setupLayoutConstraints() 282 | setupProperties() 283 | setupTextFieldBindings() 284 | } 285 | 286 | public required init?(coder: NSCoder) { 287 | fatalError("init(coder:) has not been implemented") 288 | } 289 | 290 | // MARK: Setup 291 | 292 | private func setupViewHierarchy() { 293 | [ 294 | backSideContainer, 295 | frontSideContainer, 296 | ].forEach(addSubview) 297 | 298 | frontSideContainer.insertSubview(selectionIndicator, at: 0) 299 | } 300 | 301 | private func setupLayoutConstraints() { 302 | NSLayoutConstraint.activate([ 303 | heightConstraint, 304 | 305 | frontSideContainer.topAnchor.constraint(equalTo: topAnchor), 306 | frontSideContainer.bottomAnchor.constraint(equalTo: bottomAnchor), 307 | frontSideContainer.leadingAnchor.constraint(equalTo: leadingAnchor), 308 | frontSideContainer.trailingAnchor.constraint(equalTo: trailingAnchor), 309 | 310 | backSideContainer.topAnchor.constraint(equalTo: topAnchor), 311 | backSideContainer.bottomAnchor.constraint(equalTo: bottomAnchor), 312 | backSideContainer.leadingAnchor.constraint(equalTo: leadingAnchor), 313 | backSideContainer.trailingAnchor.constraint(equalTo: trailingAnchor), 314 | ]) 315 | } 316 | 317 | private func setupProperties() { 318 | tapGestureRecognizer.addTarget(self, action: #selector(finishEditing)) 319 | addGestureRecognizer(tapGestureRecognizer) 320 | 321 | translatesAutoresizingMaskIntoConstraints = false 322 | layer.cornerRadius = 16 323 | layer.masksToBounds = true 324 | } 325 | 326 | private func setupTextFieldBindings() { 327 | frontSideContainer.cardNumberField.addTarget(self, action: #selector(cardNumberEditingChanged), for: .editingChanged) 328 | frontSideContainer.cardholderNameField.addTarget(self, action: #selector(cardholderNameEditingChanged), for: .editingChanged) 329 | frontSideContainer.validityDateField.addTarget(self, action: #selector(validityDateEditingChanged), for: .editingChanged) 330 | backSideContainer.CVVNumberField.addTarget(self, action: #selector(CVVNumberEditingChanged), for: .editingChanged) 331 | frontSideContainer.cardNumberField.addTarget(self, action: #selector(cardNumberEditingBegan), for: .editingDidBegin) 332 | frontSideContainer.cardholderNameField.addTarget(self, action: #selector(cardholderNameEditingBegan), for: .editingDidBegin) 333 | frontSideContainer.validityDateField.addTarget(self, action: #selector(validityDateEditingBegan), for: .editingDidBegin) 334 | backSideContainer.CVVNumberField.addTarget(self, action: #selector(CVVNumberEditingBegan), for: .editingDidBegin) 335 | } 336 | 337 | // MARK: Private 338 | 339 | /// Performs aniamted rotation of Credit Card to show opposite side. 340 | /// - Parameters: 341 | /// - duration: Indicates how long should be the animation. 342 | private func flip(with duration: Double = 0.5) { 343 | 344 | var transform = CATransform3DIdentity 345 | transform.m34 = 1.0 / 500.0 346 | 347 | UIViewPropertyAnimator.runningPropertyAnimator(withDuration: duration / 2, delay: 0, options: .curveLinear, animations: { 348 | transform = CATransform3DRotate(transform, (self.backSideContainer.isHidden ? 1 : -1) * CGFloat.pi / 2, 0, self.backSideContainer.isHidden ? 1 : -1, 0) 349 | self.layer.transform = transform 350 | }, completion: { _ in 351 | self.frontSideContainer.isHidden.toggle() 352 | self.backSideContainer.isHidden.toggle() 353 | 354 | if self.frontSideContainer.isHidden { 355 | self.backSideContainer.insertSubview(self.selectionIndicator, belowSubview: self.backSideContainer.CVVNumberField) 356 | } else { 357 | self.frontSideContainer.insertSubview(self.selectionIndicator, at: 0) 358 | } 359 | self.updateIndicator(animated: false) 360 | 361 | UIViewPropertyAnimator.runningPropertyAnimator(withDuration: duration / 2, delay: 0, options: .curveLinear, animations: { 362 | transform = CATransform3DRotate(transform, CGFloat.pi / 2, 0, self.backSideContainer.isHidden ? -1 : 1, 0) 363 | self.layer.transform = transform 364 | }) 365 | }) 366 | } 367 | 368 | // MARK: Text Field Bindings 369 | 370 | @objc private func cardNumberEditingChanged() { 371 | guard let newValue = frontSideContainer.cardNumberField.text else { return } 372 | creditCardDataDelegate?.cardNumberChanged(newValue) 373 | } 374 | 375 | @objc private func cardholderNameEditingChanged() { 376 | guard let newValue = frontSideContainer.cardholderNameField.text else { return } 377 | creditCardDataDelegate?.cardholderNameChanged(newValue) 378 | } 379 | 380 | @objc private func validityDateEditingChanged() { 381 | guard let newValue = frontSideContainer.validityDateField.text else { return } 382 | creditCardDataDelegate?.validityDateChanged(newValue) 383 | } 384 | 385 | @objc private func CVVNumberEditingChanged() { 386 | guard let newValue = backSideContainer.CVVNumberField.text else { return } 387 | creditCardDataDelegate?.CVVNumberChanged(newValue) 388 | } 389 | 390 | @objc private func cardNumberEditingBegan() { 391 | shouldTextFieldsBecomeFirstResponder = true 392 | creditCardDataDelegate?.beganEditing(in: .cardNumber) 393 | } 394 | 395 | @objc private func cardholderNameEditingBegan() { 396 | shouldTextFieldsBecomeFirstResponder = true 397 | creditCardDataDelegate?.beganEditing(in: .cardholderName) 398 | } 399 | 400 | @objc private func validityDateEditingBegan() { 401 | shouldTextFieldsBecomeFirstResponder = true 402 | creditCardDataDelegate?.beganEditing(in: .validityDate) 403 | } 404 | 405 | @objc private func CVVNumberEditingBegan() { 406 | shouldTextFieldsBecomeFirstResponder = true 407 | creditCardDataDelegate?.beganEditing(in: .CVVNumber) 408 | } 409 | } 410 | 411 | // MARK: CardViewInputDelegate 412 | 413 | extension CardView: CardViewInputDelegate { 414 | 415 | /// Updates frame of selection indicator to current Text Field. 416 | /// - Parameters: 417 | /// - animated: Indicates if update should be animated. 418 | func updateIndicator(animated: Bool) { 419 | selectionIndicator.isHidden = false 420 | let newFrame: CGRect = { 421 | switch currentInput { 422 | case .cardNumber: 423 | return frontSideContainer.cardNumberField.frame 424 | case .cardholderName: 425 | return frontSideContainer.cardholderNameField.frame 426 | case .validityDate: 427 | return frontSideContainer.validityDateField.frame 428 | case .CVVNumber: 429 | return backSideContainer.CVVNumberField.frame 430 | case .none: 431 | return .zero 432 | } 433 | }() 434 | if selectionIndicator.frame == CGRect.zero.insetBy(dx: -8, dy: -4) { 435 | selectionIndicator.frame = CGRect(x: newFrame.minX, y: newFrame.minY, width: 5, height: newFrame.height) 436 | } 437 | if animated && currentInput != .none { 438 | UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.2, delay: 0, options: .curveLinear, animations: { 439 | self.selectionIndicator.frame = newFrame.insetBy(dx: -8, dy: -4) 440 | }) 441 | } else { 442 | selectionIndicator.frame = newFrame.insetBy(dx: -8, dy: -4) 443 | } 444 | } 445 | 446 | /// Updates current selection to given type. 447 | /// - Parameters: 448 | /// - type: TextFieldType value for selected Text Field. 449 | public func updateCurrentInput(to type: TextFieldType) { 450 | guard type != currentInput else { return } 451 | currentInput = type 452 | } 453 | 454 | /// Changes current selection to next Text Field in flow if possible. 455 | public func moveToNextTextField() { 456 | guard let nextInput = TextFieldType(rawValue: currentInput.rawValue + 1) else { return } 457 | updateCurrentInput(to: nextInput) 458 | } 459 | 460 | /// Changes current selection to previous Text Field in flow if possible. 461 | public func moveToPreviousTextField() { 462 | guard let prevInput = TextFieldType(rawValue: currentInput.rawValue - 1) else { return } 463 | updateCurrentInput(to: prevInput) 464 | } 465 | 466 | /// Finished editing by hiding keyboard and selection idnicator. 467 | @objc public func finishEditing() { 468 | endEditing(true) 469 | selectionIndicator.frame = .zero 470 | selectionIndicator.isHidden = true 471 | currentInput = .none 472 | creditCardDataDelegate?.beganEditing(in: .cardNumber) 473 | } 474 | 475 | /// Updates Card Provider icon based on Card number input. 476 | /// - Parameters: 477 | /// - cardNumber: Card number for provider recognition. 478 | public func updateCardProvider(cardNumber: String) { 479 | currentCardProvider = CardProvider.recognizeProvider(from: cardNumber) 480 | } 481 | 482 | public func updateSelectionIndicator() { 483 | updateIndicator(animated: true) 484 | } 485 | } 486 | 487 | // MARK: CreditCardDataDelegate 488 | 489 | extension CardView: CreditCardDataDelegate { 490 | 491 | /// - SeeAlso: CreditCardDataDelegate.beganEditing(textFieldType:) 492 | public func beganEditing(in textFieldType: TextFieldType) { 493 | shouldTextFieldsBecomeFirstResponder = false 494 | currentInput = textFieldType 495 | } 496 | 497 | /// - SeeAlso: CreditCardDataDelegate.cardNumberChanged(number:) 498 | public func cardNumberChanged(_ number: String) { 499 | frontSideContainer.cardNumberField.inputHasChanged(to: number) 500 | updateCardProvider(cardNumber: number) 501 | } 502 | 503 | /// - SeeAlso: CreditCardDataDelegate.cardholderNameChanged(name:) 504 | public func cardholderNameChanged(_ name: String) { 505 | frontSideContainer.cardholderNameField.text = name 506 | frontSideContainer.cardholderNameField.sendActions(for: .editingChanged) 507 | } 508 | 509 | /// - SeeAlso: CreditCardDataDelegate.validityDateChanged(date:) 510 | public func validityDateChanged(_ date: String) { 511 | frontSideContainer.validityDateField.inputHasChanged(to: date) 512 | } 513 | 514 | /// - SeeAlso: CreditCardDataDelegate.CVVNumberChanged(cvv:) 515 | public func CVVNumberChanged(_ cvv: String) { 516 | backSideContainer.CVVNumberField.inputHasChanged(to: cvv) 517 | } 518 | } 519 | 520 | // MARK: CreditCardDataProvider 521 | 522 | extension CardView: CreditCardDataProvider { 523 | 524 | /// - SeeAlso: CreditCardDataProvider.creditCardData 525 | public var creditCardData: CreditCardData { 526 | guard 527 | let cardNumber = frontSideContainer.cardNumberField.text, 528 | let cardholderName = frontSideContainer.cardholderNameField.text, 529 | var validityDate = frontSideContainer.validityDateField.text, 530 | let CVVNumber = backSideContainer.CVVNumberField.text 531 | else { 532 | return CreditCardData() 533 | } 534 | if validityDate.count > 2 { 535 | validityDate.insert(contentsOf: validityDateSeparator, at: validityDate.index(validityDate.startIndex, offsetBy: 2)) 536 | } 537 | return CreditCardData( 538 | cardProvider: currentCardProvider, 539 | cardNumber: cardNumber, 540 | cardholderName: cardholderName, 541 | validityDate: validityDate, 542 | CVVNumber: CVVNumber 543 | ) 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Card View/CustomInputField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomInputField.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | 8 | /// Protocol handling change of Text Field Selection 9 | protocol CustomInputSelectionDelegate: AnyObject { 10 | func updateCurrentFieldSelection(to textField: UITextField) 11 | } 12 | 13 | /// Custom Text Field used for displaying user input directly on Credit Card. 14 | internal final class CustomInputField: UITextField { 15 | 16 | // MARK: Properties overrides 17 | 18 | /// Color of displayed text. 19 | override var textColor: UIColor? { 20 | willSet { 21 | (labelsStackView.arrangedSubviews as? [UILabel])?.forEach { 22 | $0.textColor = newValue 23 | } 24 | } 25 | } 26 | 27 | /// Font of displayed text. 28 | override var font: UIFont? { 29 | willSet { 30 | (labelsStackView.arrangedSubviews as? [UILabel])?.forEach { 31 | $0.font = newValue 32 | } 33 | } 34 | } 35 | 36 | // MARK: Properties 37 | 38 | /// - seeAlso: CustomInputSelectionDelegate 39 | weak var selectionDelegate: CustomInputSelectionDelegate? 40 | 41 | /// Indicates if input should be masked. 42 | internal var isSecureMode: Bool = false { 43 | didSet { 44 | updateLabels() 45 | } 46 | } 47 | 48 | /// Indicates if input should be checked for valid date value. 49 | internal var validatesDateInput: Bool = false { 50 | didSet { 51 | validateDate() 52 | } 53 | } 54 | 55 | /// Character used as input chunks separator. 56 | internal var separator: String = " " { 57 | didSet { 58 | updateLabels() 59 | if validatesDateInput { 60 | dateFormatter.dateFormat = "MM\(secureSeparator)YY" 61 | } 62 | } 63 | } 64 | 65 | /// Character used as input empty character. 66 | internal var emptyCharacter: String = "x" { 67 | didSet { 68 | updateLabels() 69 | } 70 | } 71 | 72 | /// Empty Character value that for sure is not a number and contains only one character. 73 | internal var safeEmptyCharacter: String { 74 | guard 75 | !emptyCharacter.isEmpty, 76 | emptyCharacter.count == 1, 77 | emptyCharacter.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) != nil 78 | else { return "x" } 79 | return emptyCharacter 80 | } 81 | 82 | /// String used as custom placeholder instead of Empty Character. 83 | internal var customPlaceholder: String? { 84 | didSet { 85 | updateLabels() 86 | } 87 | } 88 | 89 | /// Separator value that for sure is not a number and contains only one character. 90 | private var secureSeparator: String { 91 | guard 92 | !separator.isEmpty, 93 | separator.count == 1, 94 | separator.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) != nil 95 | else { return " " } 96 | return separator 97 | } 98 | 99 | /// Indicates maximum length of input. 100 | private let digitsLimit: Int 101 | 102 | /// Indicates format of card number, e.g. [4, 3] means that number of length 7 will be split 103 | /// into two parts of length 4 and 3 respectively (XXXX XXX). 104 | private let chunkLengths: [Int] 105 | 106 | private lazy var dateFormatter = DateFormatter() 107 | 108 | // MARK: Hierarchy 109 | 110 | /// Collection of Labels that displays current input. 111 | private var inputLabels: [UILabel] { 112 | labelsStackView.arrangedSubviews as? [CustomInputLabel] ?? [] 113 | } 114 | 115 | /// Container for inputLabels that spaces them equally. 116 | private lazy var labelsStackView: UIStackView = { 117 | let view = UIStackView() 118 | view.translatesAutoresizingMaskIntoConstraints = false 119 | view.axis = .horizontal 120 | view.alignment = .fill 121 | view.distribution = .fillEqually 122 | 123 | for _ in 0..<(digitsLimit + max(1, chunkLengths.count) - 1) { 124 | view.addArrangedSubview(CustomInputLabel()) 125 | } 126 | 127 | return view 128 | }() 129 | 130 | /// Taps handler for Text Field selection. 131 | private lazy var tapGestureRecognizer: UITapGestureRecognizer = { 132 | let tapRecognizer = UITapGestureRecognizer() 133 | tapRecognizer.addTarget(self, action: #selector(becomeFirstResponder)) 134 | return tapRecognizer 135 | }() 136 | 137 | // MARK: Initializers 138 | 139 | /// Initializes CustomInputField. 140 | /// - Parameters: 141 | /// - digitsLimit: Indicates maximum length of tet field input. Defaults to 16. 142 | /// - chunkLengths: Indicates format of text field input, 143 | /// e.g. [4, 3] means that number of length 7 will be split 144 | /// into two parts of length 4 and 3 respectively (XXXX XXX). 145 | init( 146 | digitsLimit: Int = 16, 147 | chunkLengths: [Int] = [] 148 | ) { 149 | self.digitsLimit = digitsLimit 150 | 151 | let secureChunkLengths: [Int] = { 152 | guard !chunkLengths.isEmpty else { return [] } 153 | 154 | var secureChunkLengths: [Int] = chunkLengths 155 | while secureChunkLengths.reduce(0, +) > digitsLimit { 156 | secureChunkLengths = secureChunkLengths.dropLast() 157 | } 158 | 159 | for index in 1.. Bool { 260 | guard let currentText = textField.text, 261 | let range = Range(range, in: currentText) else { return false } 262 | 263 | return currentText.replacingCharacters(in: range, with: string).count <= digitsLimit || digitsLimit == 0 264 | } 265 | } 266 | 267 | // MARK: DigitInputLabel 268 | 269 | /// Label used for digit display in CustomInputField. 270 | private final class CustomInputLabel: UILabel { 271 | 272 | // MARK: Initializers 273 | 274 | /// Initializes instance of CustomInputLabel 275 | init() { 276 | super.init(frame: .zero) 277 | 278 | setupProperties() 279 | } 280 | 281 | required init?(coder: NSCoder) { 282 | fatalError("init(coder:) has not been implemented") 283 | } 284 | 285 | // MARK: Private 286 | 287 | private func setupProperties() { 288 | textAlignment = .center 289 | font = UIFont.systemFont(ofSize: 30) 290 | isUserInteractionEnabled = true 291 | layer.cornerRadius = 5 292 | layer.masksToBounds = true 293 | translatesAutoresizingMaskIntoConstraints = false 294 | adjustsFontSizeToFitWidth = true 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Models/CardProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardProvider.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | /// Represents a provider of Credit Cards. 7 | public class CardProvider { 8 | 9 | // MARK: Parameters 10 | 11 | /// Name of Card Provider. 12 | public let name: String 13 | 14 | /// Icon with logo of Card Provider. 15 | public let icon: UIImage? 16 | 17 | /// String pattern for recognizing card brand from the card number. 18 | private let pattern: String 19 | 20 | /// Regular Expression for recognizing card brand from the card number. 21 | private var regex: NSRegularExpression? { 22 | try? NSRegularExpression(pattern: "^\(pattern)\\d*") 23 | } 24 | 25 | // MARK: Providers Collections 26 | 27 | /// Collection of card providers added by the application. 28 | public static var customCardProviders = Set() 29 | 30 | /// Collection of default Card Providers. 31 | private static let defaultProfiders: [CardProvider] = [ 32 | /// VISA starts with `4`. 33 | CardProvider(name: "Visa", assetName: "visa.png", pattern: "4"), 34 | /// MasterCard starts with `5`. 35 | CardProvider(name: "Mastercard", assetName: "mastercard.png", pattern: "5"), 36 | /// Discover starts with `6`. 37 | CardProvider(name: "Discover", assetName: "discover.png", pattern: "6"), 38 | /// American Express starts with `34` or `37`. 39 | CardProvider(name: "American Express", assetName: "american_express.png", pattern: "3[4,7]"), 40 | /// Diners Club starts with `30`, `36` or `38`. 41 | CardProvider(name: "Diners Club", assetName: "diners_club.png", pattern: "3[0,6,8]"), 42 | /// JCB starts with `35`. 43 | CardProvider(name: "JCB", assetName: "jcb.png", pattern: "35"), 44 | ] 45 | 46 | // MARK: Initializers 47 | 48 | /// Initializes instance of Card Provider. 49 | /// 50 | /// - Parameters: 51 | /// - name: name of Card Provider. 52 | /// - icon: optional icon of Card Provider. 53 | /// - pattern: pattern for recognizing Card Provider from card number. This will be applied as a regex `^\(pattern)\\d*`. 54 | init(name: String, icon: UIImage?, pattern: String) { 55 | self.name = name 56 | self.icon = icon 57 | self.pattern = pattern 58 | } 59 | 60 | /// Initializes instance of default Card Provider. 61 | /// - Parameters: 62 | /// - name: name of Card Provider. 63 | /// - assetName: name of asset for Card Provider icon. 64 | /// - pattern: pattern for recognizing Card Provider from card number. 65 | private init(name: String, assetName: String, pattern: String) { 66 | self.name = name 67 | self.pattern = pattern 68 | self.icon = { 69 | let resourceBundle: Bundle? = { 70 | let frameworkBundle = Bundle(for: CardView.self) 71 | if let resourceBundlePath = frameworkBundle.path(forResource: "AnimatedCardInput", ofType: "bundle") { 72 | return Bundle(path: resourceBundlePath) 73 | } 74 | return frameworkBundle 75 | }() 76 | return UIImage(named: assetName, in: resourceBundle, compatibleWith: nil) 77 | }() 78 | } 79 | 80 | // MARK: Static 81 | 82 | /// Add custom providers to the collection used for recognition from card number. 83 | /// - Parameters: 84 | /// - providers: Collection of custom providers to add for brand recognition. 85 | public static func addCardProviders(_ providers: CardProvider...) { 86 | providers.forEach { customCardProviders.insert($0) } 87 | } 88 | 89 | /// Recognizes `Card Provider` from the card number. 90 | /// - Parameters: 91 | /// - cardNumber: Card Number to recognize from. 92 | /// - Returns: `Card Provider` object if card number matches any of available patterns. 93 | public static func recognizeProvider(from cardNumber: String) -> CardProvider? { 94 | let range = NSRange(location: 0, length: cardNumber.utf16.count) 95 | for provider in defaultProfiders + customCardProviders { 96 | if provider.regex?.firstMatch(in: cardNumber, options: [], range: range) != nil { 97 | return provider 98 | } 99 | } 100 | return nil 101 | } 102 | } 103 | 104 | // MARK: Hashable 105 | 106 | extension CardProvider: Hashable { 107 | 108 | /// - SeeAlso: Hashable.hash(into:) 109 | public func hash(into hasher: inout Hasher) { 110 | hasher.combine(name) 111 | hasher.combine(pattern) 112 | } 113 | 114 | /// - SeeAlso: Equatable.==(lhs:, rhs:) 115 | public static func == (lhs: CardProvider, rhs: CardProvider) -> Bool { 116 | lhs.name == rhs.name && lhs.pattern == rhs.pattern 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Models/CreditCardData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardData.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import Foundation 7 | 8 | public struct CreditCardData { 9 | 10 | /// Object representing card's provider. 11 | public var cardProvider: CardProvider? 12 | 13 | /// String representing Number of the Credit Card. 14 | public var cardNumber: String = "" 15 | 16 | /// String representing Name of the Cardholder's. 17 | public var cardholderName: String = "" 18 | 19 | /// String representing Validity Date of the Credit Card. 20 | public var validityDate: String = "" 21 | 22 | /// String representing CVV Number of the Credit Card. 23 | public var CVVNumber: String = "" 24 | } 25 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Models/TextFieldType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldType.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | /// Types of available text fields, used to handle progressing though input flow. 7 | public enum TextFieldType: Int, CaseIterable { 8 | case cardNumber = 0 9 | case cardholderName 10 | case validityDate 11 | case CVVNumber 12 | 13 | case none 14 | } 15 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Protocols/CardViewInputdelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardViewInputdelegate.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | /// Protocol for handling text field interactions. 7 | public protocol CardViewInputDelegate: AnyObject { 8 | 9 | /// Informs `Card View` about currently selected field. 10 | /// - Parameters: 11 | /// - type: type of selected field. 12 | func updateCurrentInput(to type: TextFieldType) 13 | 14 | /// Informs that focus should be updated to next field in `TextFieldType` order. 15 | func moveToNextTextField() 16 | 17 | /// Informs that focus should be updated to previous field in `TextFieldType` order. 18 | func moveToPreviousTextField() 19 | 20 | /// Informs that finishing has been finished. 21 | func finishEditing() 22 | 23 | /// Informs `Card View` about card number changes used for updating card provider icon. 24 | /// - Parameters: 25 | /// - cardNumber: current input of `Card Number` field. 26 | func updateCardProvider(cardNumber: String) 27 | 28 | /// Informs `Card View` that selection indicator should be updated to focused field. 29 | func updateSelectionIndicator() 30 | } 31 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Protocols/CreditCardDataDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardDataDelegate.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | /// Protocol used to inform paired views about input changes. 7 | public protocol CreditCardDataDelegate: AnyObject { 8 | 9 | /// Informs that user began input of data for given Field Type. 10 | /// - Parameters: 11 | /// - textFieldType: Type of Text Field that is currently being edited. 12 | func beganEditing(in textFieldType: TextFieldType) 13 | 14 | /// Informs that card number input has changed. 15 | /// - Parameters: 16 | /// - number: String value of changed Card Number. 17 | func cardNumberChanged(_ number: String) 18 | 19 | /// Informs that cardholder's name input has changed. 20 | /// - Parameters: 21 | /// - name: String value of changed Cardholder Name. 22 | func cardholderNameChanged(_ name: String) 23 | 24 | /// Informs that card's validity date input has changed. 25 | /// - Parameters: 26 | /// - date: String value of changed Card Validity Date. 27 | func validityDateChanged(_ date: String) 28 | 29 | /// Informs that card's CVV number input has changed. 30 | /// - Parameters: 31 | /// - cvv: String value of changed CVV Number. 32 | func CVVNumberChanged(_ cvv: String) 33 | } 34 | -------------------------------------------------------------------------------- /AnimatedCardInput/Classes/Protocols/CreditCardDataProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardDataProvider.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | /// Protocol for retrieving all user inputs as `Credit Card Data` object. 7 | public protocol CreditCardDataProvider: AnyObject { 8 | 9 | /// Object representing data of the Credit Card. 10 | var creditCardData: CreditCardData { get } 11 | } 12 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 11 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 12 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 13 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 14 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 15 | 607FACEC1AFB9204008FA782 /* CreditCardDataProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACEB1AFB9204008FA782 /* CreditCardDataProviderTests.swift */; }; 16 | AE38FF99247198DA00B3225C /* CustomInputFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE38FF98247198D900B3225C /* CustomInputFieldTests.swift */; }; 17 | AE3A18CE24751A3300E37996 /* CardViewInputDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3A18CC24751A3300E37996 /* CardViewInputDelegateTests.swift */; }; 18 | AE3A18CF24751A3300E37996 /* CreditCardDataDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3A18CD24751A3300E37996 /* CreditCardDataDelegateTests.swift */; }; 19 | B508179E82C172FD1603E4A8 /* Pods_AnimatedCardInput_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6769E5865DC7B5E9D7412D5C /* Pods_AnimatedCardInput_Example.framework */; }; 20 | FD753DD73087A992E9E1C376 /* Pods_AnimatedCardInput_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 59E704C03E2F4BAF3FF9283B /* Pods_AnimatedCardInput_Tests.framework */; }; 21 | /* End PBXBuildFile section */ 22 | 23 | /* Begin PBXContainerItemProxy section */ 24 | 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */ = { 25 | isa = PBXContainerItemProxy; 26 | containerPortal = 607FACC81AFB9204008FA782 /* Project object */; 27 | proxyType = 1; 28 | remoteGlobalIDString = 607FACCF1AFB9204008FA782; 29 | remoteInfo = AnimatedCardInput; 30 | }; 31 | /* End PBXContainerItemProxy section */ 32 | 33 | /* Begin PBXFileReference section */ 34 | 211A8A9268EBA1B69BDE7702 /* Pods-AnimatedCardInput_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AnimatedCardInput_Tests.debug.xcconfig"; path = "Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests.debug.xcconfig"; sourceTree = ""; }; 35 | 4D937B8F77FEF77CE9C74799 /* Pods-AnimatedCardInput_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AnimatedCardInput_Example.release.xcconfig"; path = "Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example.release.xcconfig"; sourceTree = ""; }; 36 | 59E704C03E2F4BAF3FF9283B /* Pods_AnimatedCardInput_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AnimatedCardInput_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | 607FACD01AFB9204008FA782 /* AnimatedCardInput_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AnimatedCardInput_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 38 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 40 | 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 41 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 43 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 44 | 607FACE51AFB9204008FA782 /* AnimatedCardInput_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AnimatedCardInput_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 45 | 607FACEA1AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46 | 607FACEB1AFB9204008FA782 /* CreditCardDataProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardDataProviderTests.swift; sourceTree = ""; }; 47 | 6769E5865DC7B5E9D7412D5C /* Pods_AnimatedCardInput_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AnimatedCardInput_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | 86E80C89903F568B4327324F /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; 49 | A595B7F04A7EB795534BE567 /* Pods-AnimatedCardInput_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AnimatedCardInput_Example.debug.xcconfig"; path = "Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example.debug.xcconfig"; sourceTree = ""; }; 50 | AE38FF98247198D900B3225C /* CustomInputFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomInputFieldTests.swift; sourceTree = ""; }; 51 | AE3A18CC24751A3300E37996 /* CardViewInputDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CardViewInputDelegateTests.swift; sourceTree = ""; }; 52 | AE3A18CD24751A3300E37996 /* CreditCardDataDelegateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreditCardDataDelegateTests.swift; sourceTree = ""; }; 53 | B4E957734DB56C1E754269F8 /* AnimatedCardInput.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = AnimatedCardInput.podspec; path = ../AnimatedCardInput.podspec; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 54 | B60AFA012BB9E99907EB7B97 /* Pods-AnimatedCardInput_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AnimatedCardInput_Tests.release.xcconfig"; path = "Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests.release.xcconfig"; sourceTree = ""; }; 55 | DA9BD7D65BB09F85CF1F3703 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; 56 | /* End PBXFileReference section */ 57 | 58 | /* Begin PBXFrameworksBuildPhase section */ 59 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 60 | isa = PBXFrameworksBuildPhase; 61 | buildActionMask = 2147483647; 62 | files = ( 63 | B508179E82C172FD1603E4A8 /* Pods_AnimatedCardInput_Example.framework in Frameworks */, 64 | ); 65 | runOnlyForDeploymentPostprocessing = 0; 66 | }; 67 | 607FACE21AFB9204008FA782 /* Frameworks */ = { 68 | isa = PBXFrameworksBuildPhase; 69 | buildActionMask = 2147483647; 70 | files = ( 71 | FD753DD73087A992E9E1C376 /* Pods_AnimatedCardInput_Tests.framework in Frameworks */, 72 | ); 73 | runOnlyForDeploymentPostprocessing = 0; 74 | }; 75 | /* End PBXFrameworksBuildPhase section */ 76 | 77 | /* Begin PBXGroup section */ 78 | 607FACC71AFB9204008FA782 = { 79 | isa = PBXGroup; 80 | children = ( 81 | 607FACF51AFB993E008FA782 /* Podspec Metadata */, 82 | 607FACD21AFB9204008FA782 /* Example for AnimatedCardInput */, 83 | 607FACE81AFB9204008FA782 /* Tests */, 84 | 607FACD11AFB9204008FA782 /* Products */, 85 | 70664F292391DC549B87C2B0 /* Pods */, 86 | 9EEDE8979EC0AF52EC71914E /* Frameworks */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | 607FACD11AFB9204008FA782 /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 607FACD01AFB9204008FA782 /* AnimatedCardInput_Example.app */, 94 | 607FACE51AFB9204008FA782 /* AnimatedCardInput_Tests.xctest */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | 607FACD21AFB9204008FA782 /* Example for AnimatedCardInput */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 103 | 607FACD71AFB9204008FA782 /* ViewController.swift */, 104 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 105 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 106 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 107 | 607FACD31AFB9204008FA782 /* Supporting Files */, 108 | ); 109 | name = "Example for AnimatedCardInput"; 110 | path = AnimatedCardInput; 111 | sourceTree = ""; 112 | }; 113 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 114 | isa = PBXGroup; 115 | children = ( 116 | 607FACD41AFB9204008FA782 /* Info.plist */, 117 | ); 118 | name = "Supporting Files"; 119 | sourceTree = ""; 120 | }; 121 | 607FACE81AFB9204008FA782 /* Tests */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | AE3A18CC24751A3300E37996 /* CardViewInputDelegateTests.swift */, 125 | AE3A18CD24751A3300E37996 /* CreditCardDataDelegateTests.swift */, 126 | 607FACEB1AFB9204008FA782 /* CreditCardDataProviderTests.swift */, 127 | AE38FF98247198D900B3225C /* CustomInputFieldTests.swift */, 128 | 607FACE91AFB9204008FA782 /* Supporting Files */, 129 | ); 130 | path = Tests; 131 | sourceTree = ""; 132 | }; 133 | 607FACE91AFB9204008FA782 /* Supporting Files */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 607FACEA1AFB9204008FA782 /* Info.plist */, 137 | ); 138 | name = "Supporting Files"; 139 | sourceTree = ""; 140 | }; 141 | 607FACF51AFB993E008FA782 /* Podspec Metadata */ = { 142 | isa = PBXGroup; 143 | children = ( 144 | B4E957734DB56C1E754269F8 /* AnimatedCardInput.podspec */, 145 | DA9BD7D65BB09F85CF1F3703 /* README.md */, 146 | 86E80C89903F568B4327324F /* LICENSE */, 147 | ); 148 | name = "Podspec Metadata"; 149 | sourceTree = ""; 150 | }; 151 | 70664F292391DC549B87C2B0 /* Pods */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | A595B7F04A7EB795534BE567 /* Pods-AnimatedCardInput_Example.debug.xcconfig */, 155 | 4D937B8F77FEF77CE9C74799 /* Pods-AnimatedCardInput_Example.release.xcconfig */, 156 | 211A8A9268EBA1B69BDE7702 /* Pods-AnimatedCardInput_Tests.debug.xcconfig */, 157 | B60AFA012BB9E99907EB7B97 /* Pods-AnimatedCardInput_Tests.release.xcconfig */, 158 | ); 159 | path = Pods; 160 | sourceTree = ""; 161 | }; 162 | 9EEDE8979EC0AF52EC71914E /* Frameworks */ = { 163 | isa = PBXGroup; 164 | children = ( 165 | 6769E5865DC7B5E9D7412D5C /* Pods_AnimatedCardInput_Example.framework */, 166 | 59E704C03E2F4BAF3FF9283B /* Pods_AnimatedCardInput_Tests.framework */, 167 | ); 168 | name = Frameworks; 169 | sourceTree = ""; 170 | }; 171 | /* End PBXGroup section */ 172 | 173 | /* Begin PBXNativeTarget section */ 174 | 607FACCF1AFB9204008FA782 /* AnimatedCardInput_Example */ = { 175 | isa = PBXNativeTarget; 176 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "AnimatedCardInput_Example" */; 177 | buildPhases = ( 178 | B4B092BFA4F7411F89BBBF3B /* [CP] Check Pods Manifest.lock */, 179 | 607FACCC1AFB9204008FA782 /* Sources */, 180 | 607FACCD1AFB9204008FA782 /* Frameworks */, 181 | 607FACCE1AFB9204008FA782 /* Resources */, 182 | 5F25A5992FF0F4DFF2B1543B /* [CP] Embed Pods Frameworks */, 183 | ); 184 | buildRules = ( 185 | ); 186 | dependencies = ( 187 | ); 188 | name = AnimatedCardInput_Example; 189 | productName = AnimatedCardInput; 190 | productReference = 607FACD01AFB9204008FA782 /* AnimatedCardInput_Example.app */; 191 | productType = "com.apple.product-type.application"; 192 | }; 193 | 607FACE41AFB9204008FA782 /* AnimatedCardInput_Tests */ = { 194 | isa = PBXNativeTarget; 195 | buildConfigurationList = 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "AnimatedCardInput_Tests" */; 196 | buildPhases = ( 197 | E100FB969361531248E91201 /* [CP] Check Pods Manifest.lock */, 198 | 607FACE11AFB9204008FA782 /* Sources */, 199 | 607FACE21AFB9204008FA782 /* Frameworks */, 200 | 607FACE31AFB9204008FA782 /* Resources */, 201 | ); 202 | buildRules = ( 203 | ); 204 | dependencies = ( 205 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */, 206 | ); 207 | name = AnimatedCardInput_Tests; 208 | productName = Tests; 209 | productReference = 607FACE51AFB9204008FA782 /* AnimatedCardInput_Tests.xctest */; 210 | productType = "com.apple.product-type.bundle.unit-test"; 211 | }; 212 | /* End PBXNativeTarget section */ 213 | 214 | /* Begin PBXProject section */ 215 | 607FACC81AFB9204008FA782 /* Project object */ = { 216 | isa = PBXProject; 217 | attributes = { 218 | LastSwiftUpdateCheck = 0830; 219 | LastUpgradeCheck = 0830; 220 | ORGANIZATIONNAME = CocoaPods; 221 | TargetAttributes = { 222 | 607FACCF1AFB9204008FA782 = { 223 | CreatedOnToolsVersion = 6.3.1; 224 | LastSwiftMigration = 0900; 225 | }; 226 | 607FACE41AFB9204008FA782 = { 227 | CreatedOnToolsVersion = 6.3.1; 228 | LastSwiftMigration = 0900; 229 | TestTargetID = 607FACCF1AFB9204008FA782; 230 | }; 231 | }; 232 | }; 233 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "AnimatedCardInput" */; 234 | compatibilityVersion = "Xcode 3.2"; 235 | developmentRegion = English; 236 | hasScannedForEncodings = 0; 237 | knownRegions = ( 238 | English, 239 | en, 240 | Base, 241 | ); 242 | mainGroup = 607FACC71AFB9204008FA782; 243 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 244 | projectDirPath = ""; 245 | projectRoot = ""; 246 | targets = ( 247 | 607FACCF1AFB9204008FA782 /* AnimatedCardInput_Example */, 248 | 607FACE41AFB9204008FA782 /* AnimatedCardInput_Tests */, 249 | ); 250 | }; 251 | /* End PBXProject section */ 252 | 253 | /* Begin PBXResourcesBuildPhase section */ 254 | 607FACCE1AFB9204008FA782 /* Resources */ = { 255 | isa = PBXResourcesBuildPhase; 256 | buildActionMask = 2147483647; 257 | files = ( 258 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 259 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 260 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 261 | ); 262 | runOnlyForDeploymentPostprocessing = 0; 263 | }; 264 | 607FACE31AFB9204008FA782 /* Resources */ = { 265 | isa = PBXResourcesBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | ); 269 | runOnlyForDeploymentPostprocessing = 0; 270 | }; 271 | /* End PBXResourcesBuildPhase section */ 272 | 273 | /* Begin PBXShellScriptBuildPhase section */ 274 | 5F25A5992FF0F4DFF2B1543B /* [CP] Embed Pods Frameworks */ = { 275 | isa = PBXShellScriptBuildPhase; 276 | buildActionMask = 2147483647; 277 | files = ( 278 | ); 279 | inputPaths = ( 280 | "${PODS_ROOT}/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example-frameworks.sh", 281 | "${BUILT_PRODUCTS_DIR}/AnimatedCardInput/AnimatedCardInput.framework", 282 | ); 283 | name = "[CP] Embed Pods Frameworks"; 284 | outputPaths = ( 285 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AnimatedCardInput.framework", 286 | ); 287 | runOnlyForDeploymentPostprocessing = 0; 288 | shellPath = /bin/sh; 289 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example-frameworks.sh\"\n"; 290 | showEnvVarsInLog = 0; 291 | }; 292 | B4B092BFA4F7411F89BBBF3B /* [CP] Check Pods Manifest.lock */ = { 293 | isa = PBXShellScriptBuildPhase; 294 | buildActionMask = 2147483647; 295 | files = ( 296 | ); 297 | inputFileListPaths = ( 298 | ); 299 | inputPaths = ( 300 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 301 | "${PODS_ROOT}/Manifest.lock", 302 | ); 303 | name = "[CP] Check Pods Manifest.lock"; 304 | outputFileListPaths = ( 305 | ); 306 | outputPaths = ( 307 | "$(DERIVED_FILE_DIR)/Pods-AnimatedCardInput_Example-checkManifestLockResult.txt", 308 | ); 309 | runOnlyForDeploymentPostprocessing = 0; 310 | shellPath = /bin/sh; 311 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 312 | showEnvVarsInLog = 0; 313 | }; 314 | E100FB969361531248E91201 /* [CP] Check Pods Manifest.lock */ = { 315 | isa = PBXShellScriptBuildPhase; 316 | buildActionMask = 2147483647; 317 | files = ( 318 | ); 319 | inputFileListPaths = ( 320 | ); 321 | inputPaths = ( 322 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 323 | "${PODS_ROOT}/Manifest.lock", 324 | ); 325 | name = "[CP] Check Pods Manifest.lock"; 326 | outputFileListPaths = ( 327 | ); 328 | outputPaths = ( 329 | "$(DERIVED_FILE_DIR)/Pods-AnimatedCardInput_Tests-checkManifestLockResult.txt", 330 | ); 331 | runOnlyForDeploymentPostprocessing = 0; 332 | shellPath = /bin/sh; 333 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 334 | showEnvVarsInLog = 0; 335 | }; 336 | /* End PBXShellScriptBuildPhase section */ 337 | 338 | /* Begin PBXSourcesBuildPhase section */ 339 | 607FACCC1AFB9204008FA782 /* Sources */ = { 340 | isa = PBXSourcesBuildPhase; 341 | buildActionMask = 2147483647; 342 | files = ( 343 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, 344 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 345 | ); 346 | runOnlyForDeploymentPostprocessing = 0; 347 | }; 348 | 607FACE11AFB9204008FA782 /* Sources */ = { 349 | isa = PBXSourcesBuildPhase; 350 | buildActionMask = 2147483647; 351 | files = ( 352 | AE3A18CE24751A3300E37996 /* CardViewInputDelegateTests.swift in Sources */, 353 | AE38FF99247198DA00B3225C /* CustomInputFieldTests.swift in Sources */, 354 | 607FACEC1AFB9204008FA782 /* CreditCardDataProviderTests.swift in Sources */, 355 | AE3A18CF24751A3300E37996 /* CreditCardDataDelegateTests.swift in Sources */, 356 | ); 357 | runOnlyForDeploymentPostprocessing = 0; 358 | }; 359 | /* End PBXSourcesBuildPhase section */ 360 | 361 | /* Begin PBXTargetDependency section */ 362 | 607FACE71AFB9204008FA782 /* PBXTargetDependency */ = { 363 | isa = PBXTargetDependency; 364 | target = 607FACCF1AFB9204008FA782 /* AnimatedCardInput_Example */; 365 | targetProxy = 607FACE61AFB9204008FA782 /* PBXContainerItemProxy */; 366 | }; 367 | /* End PBXTargetDependency section */ 368 | 369 | /* Begin PBXVariantGroup section */ 370 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 371 | isa = PBXVariantGroup; 372 | children = ( 373 | 607FACDA1AFB9204008FA782 /* Base */, 374 | ); 375 | name = Main.storyboard; 376 | sourceTree = ""; 377 | }; 378 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 379 | isa = PBXVariantGroup; 380 | children = ( 381 | 607FACDF1AFB9204008FA782 /* Base */, 382 | ); 383 | name = LaunchScreen.xib; 384 | sourceTree = ""; 385 | }; 386 | /* End PBXVariantGroup section */ 387 | 388 | /* Begin XCBuildConfiguration section */ 389 | 607FACED1AFB9204008FA782 /* Debug */ = { 390 | isa = XCBuildConfiguration; 391 | buildSettings = { 392 | ALWAYS_SEARCH_USER_PATHS = NO; 393 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 394 | CLANG_CXX_LIBRARY = "libc++"; 395 | CLANG_ENABLE_MODULES = YES; 396 | CLANG_ENABLE_OBJC_ARC = YES; 397 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 398 | CLANG_WARN_BOOL_CONVERSION = YES; 399 | CLANG_WARN_COMMA = YES; 400 | CLANG_WARN_CONSTANT_CONVERSION = YES; 401 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 402 | CLANG_WARN_EMPTY_BODY = YES; 403 | CLANG_WARN_ENUM_CONVERSION = YES; 404 | CLANG_WARN_INFINITE_RECURSION = YES; 405 | CLANG_WARN_INT_CONVERSION = YES; 406 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 407 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 408 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 409 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 410 | CLANG_WARN_STRICT_PROTOTYPES = YES; 411 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 412 | CLANG_WARN_UNREACHABLE_CODE = YES; 413 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 414 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 415 | COPY_PHASE_STRIP = NO; 416 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 417 | ENABLE_STRICT_OBJC_MSGSEND = YES; 418 | ENABLE_TESTABILITY = YES; 419 | GCC_C_LANGUAGE_STANDARD = gnu99; 420 | GCC_DYNAMIC_NO_PIC = NO; 421 | GCC_NO_COMMON_BLOCKS = YES; 422 | GCC_OPTIMIZATION_LEVEL = 0; 423 | GCC_PREPROCESSOR_DEFINITIONS = ( 424 | "DEBUG=1", 425 | "$(inherited)", 426 | ); 427 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 428 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 429 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 430 | GCC_WARN_UNDECLARED_SELECTOR = YES; 431 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 432 | GCC_WARN_UNUSED_FUNCTION = YES; 433 | GCC_WARN_UNUSED_VARIABLE = YES; 434 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 435 | MTL_ENABLE_DEBUG_INFO = YES; 436 | ONLY_ACTIVE_ARCH = YES; 437 | SDKROOT = iphoneos; 438 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 439 | }; 440 | name = Debug; 441 | }; 442 | 607FACEE1AFB9204008FA782 /* Release */ = { 443 | isa = XCBuildConfiguration; 444 | buildSettings = { 445 | ALWAYS_SEARCH_USER_PATHS = NO; 446 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 447 | CLANG_CXX_LIBRARY = "libc++"; 448 | CLANG_ENABLE_MODULES = YES; 449 | CLANG_ENABLE_OBJC_ARC = YES; 450 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 451 | CLANG_WARN_BOOL_CONVERSION = YES; 452 | CLANG_WARN_COMMA = YES; 453 | CLANG_WARN_CONSTANT_CONVERSION = YES; 454 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 455 | CLANG_WARN_EMPTY_BODY = YES; 456 | CLANG_WARN_ENUM_CONVERSION = YES; 457 | CLANG_WARN_INFINITE_RECURSION = YES; 458 | CLANG_WARN_INT_CONVERSION = YES; 459 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 460 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 461 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 462 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 463 | CLANG_WARN_STRICT_PROTOTYPES = YES; 464 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 465 | CLANG_WARN_UNREACHABLE_CODE = YES; 466 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 467 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 468 | COPY_PHASE_STRIP = NO; 469 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 470 | ENABLE_NS_ASSERTIONS = NO; 471 | ENABLE_STRICT_OBJC_MSGSEND = YES; 472 | GCC_C_LANGUAGE_STANDARD = gnu99; 473 | GCC_NO_COMMON_BLOCKS = YES; 474 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 475 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 476 | GCC_WARN_UNDECLARED_SELECTOR = YES; 477 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 478 | GCC_WARN_UNUSED_FUNCTION = YES; 479 | GCC_WARN_UNUSED_VARIABLE = YES; 480 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 481 | MTL_ENABLE_DEBUG_INFO = NO; 482 | SDKROOT = iphoneos; 483 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 484 | VALIDATE_PRODUCT = YES; 485 | }; 486 | name = Release; 487 | }; 488 | 607FACF01AFB9204008FA782 /* Debug */ = { 489 | isa = XCBuildConfiguration; 490 | baseConfigurationReference = A595B7F04A7EB795534BE567 /* Pods-AnimatedCardInput_Example.debug.xcconfig */; 491 | buildSettings = { 492 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 493 | INFOPLIST_FILE = AnimatedCardInput/Info.plist; 494 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 495 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 496 | MODULE_NAME = ExampleApp; 497 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 498 | PRODUCT_NAME = "$(TARGET_NAME)"; 499 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 500 | SWIFT_VERSION = 5.0; 501 | TARGETED_DEVICE_FAMILY = "1,2"; 502 | }; 503 | name = Debug; 504 | }; 505 | 607FACF11AFB9204008FA782 /* Release */ = { 506 | isa = XCBuildConfiguration; 507 | baseConfigurationReference = 4D937B8F77FEF77CE9C74799 /* Pods-AnimatedCardInput_Example.release.xcconfig */; 508 | buildSettings = { 509 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 510 | INFOPLIST_FILE = AnimatedCardInput/Info.plist; 511 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 512 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 513 | MODULE_NAME = ExampleApp; 514 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.demo.$(PRODUCT_NAME:rfc1034identifier)"; 515 | PRODUCT_NAME = "$(TARGET_NAME)"; 516 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 517 | SWIFT_VERSION = 5.0; 518 | TARGETED_DEVICE_FAMILY = "1,2"; 519 | }; 520 | name = Release; 521 | }; 522 | 607FACF31AFB9204008FA782 /* Debug */ = { 523 | isa = XCBuildConfiguration; 524 | baseConfigurationReference = 211A8A9268EBA1B69BDE7702 /* Pods-AnimatedCardInput_Tests.debug.xcconfig */; 525 | buildSettings = { 526 | FRAMEWORK_SEARCH_PATHS = ( 527 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 528 | "$(inherited)", 529 | ); 530 | GCC_PREPROCESSOR_DEFINITIONS = ( 531 | "DEBUG=1", 532 | "$(inherited)", 533 | ); 534 | INFOPLIST_FILE = Tests/Info.plist; 535 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 536 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 537 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 538 | PRODUCT_NAME = "$(TARGET_NAME)"; 539 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 540 | SWIFT_VERSION = 4.0; 541 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AnimatedCardInput_Example.app/AnimatedCardInput_Example"; 542 | }; 543 | name = Debug; 544 | }; 545 | 607FACF41AFB9204008FA782 /* Release */ = { 546 | isa = XCBuildConfiguration; 547 | baseConfigurationReference = B60AFA012BB9E99907EB7B97 /* Pods-AnimatedCardInput_Tests.release.xcconfig */; 548 | buildSettings = { 549 | FRAMEWORK_SEARCH_PATHS = ( 550 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 551 | "$(inherited)", 552 | ); 553 | INFOPLIST_FILE = Tests/Info.plist; 554 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 555 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 556 | PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.$(PRODUCT_NAME:rfc1034identifier)"; 557 | PRODUCT_NAME = "$(TARGET_NAME)"; 558 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 559 | SWIFT_VERSION = 4.0; 560 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AnimatedCardInput_Example.app/AnimatedCardInput_Example"; 561 | }; 562 | name = Release; 563 | }; 564 | /* End XCBuildConfiguration section */ 565 | 566 | /* Begin XCConfigurationList section */ 567 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "AnimatedCardInput" */ = { 568 | isa = XCConfigurationList; 569 | buildConfigurations = ( 570 | 607FACED1AFB9204008FA782 /* Debug */, 571 | 607FACEE1AFB9204008FA782 /* Release */, 572 | ); 573 | defaultConfigurationIsVisible = 0; 574 | defaultConfigurationName = Release; 575 | }; 576 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "AnimatedCardInput_Example" */ = { 577 | isa = XCConfigurationList; 578 | buildConfigurations = ( 579 | 607FACF01AFB9204008FA782 /* Debug */, 580 | 607FACF11AFB9204008FA782 /* Release */, 581 | ); 582 | defaultConfigurationIsVisible = 0; 583 | defaultConfigurationName = Release; 584 | }; 585 | 607FACF21AFB9204008FA782 /* Build configuration list for PBXNativeTarget "AnimatedCardInput_Tests" */ = { 586 | isa = XCConfigurationList; 587 | buildConfigurations = ( 588 | 607FACF31AFB9204008FA782 /* Debug */, 589 | 607FACF41AFB9204008FA782 /* Release */, 590 | ); 591 | defaultConfigurationIsVisible = 0; 592 | defaultConfigurationName = Release; 593 | }; 594 | /* End XCConfigurationList section */ 595 | }; 596 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 597 | } 598 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput.xcodeproj/xcshareddata/xcschemes/AnimatedCardInput-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 82 | 88 | 89 | 90 | 91 | 92 | 93 | 99 | 101 | 107 | 108 | 109 | 110 | 112 | 113 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | 8 | @UIApplicationMain 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | 11 | var window: UIWindow? 12 | 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | true 15 | } 16 | 17 | func applicationWillResignActive(_ application: UIApplication) {} 18 | func applicationDidEnterBackground(_ application: UIApplication) {} 19 | func applicationWillEnterForeground(_ application: UIApplication) {} 20 | func applicationDidBecomeActive(_ application: UIApplication) {} 21 | func applicationWillTerminate(_ application: UIApplication) {} 22 | } 23 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput/Images.xcassets/netguru.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "netguru-logo-single-n.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput/Images.xcassets/netguru.imageset/netguru-logo-single-n.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netguru/AnimatedCardInput/f5dcb70770491525a6b12182ac333f3aea5a1c1e/Example/AnimatedCardInput/Images.xcassets/netguru.imageset/netguru-logo-single-n.png -------------------------------------------------------------------------------- /Example/AnimatedCardInput/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Example/AnimatedCardInput/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AnimatedCardInput 4 | // 5 | 6 | import UIKit 7 | import AnimatedCardInput 8 | 9 | class ViewController: UIViewController { 10 | 11 | private let cardView: CardView = { 12 | let view = CardView( 13 | cardNumberDigitsLimit: 16, 14 | cardNumberChunkLengths: [4, 4, 4, 4], 15 | CVVNumberDigitsLimit: 3 16 | ) 17 | 18 | view.frontSideCardColor = #colorLiteral(red: 0.2156862745, green: 0.2156862745, blue: 0.2156862745, alpha: 1) 19 | view.backSideCardColor = #colorLiteral(red: 0.2156862745, green: 0.2156862745, blue: 0.2156862745, alpha: 1) 20 | view.selectionIndicatorColor = .orange 21 | view.frontSideTextColor = .white 22 | view.CVVBackgroundColor = .white 23 | view.backSideTextColor = .black 24 | view.isSecureInput = true 25 | 26 | view.numberInputFont = UIFont.systemFont(ofSize: 24, weight: .semibold) 27 | view.nameInputFont = UIFont.systemFont(ofSize: 14, weight: .regular) 28 | view.validityInputFont = UIFont.systemFont(ofSize: 14, weight: .regular) 29 | view.CVVInputFont = UIFont.systemFont(ofSize: 20, weight: .semibold) 30 | 31 | return view 32 | }() 33 | 34 | private let inputsView: CardInputsView = { 35 | let view = CardInputsView(cardNumberDigitLimit: 16) 36 | view.translatesAutoresizingMaskIntoConstraints = false 37 | view.isSecureInput = true 38 | return view 39 | }() 40 | 41 | private let retrieveButton: UIButton = { 42 | let button = UIButton(type: .custom) 43 | button.setTitle(" Retrieve data ", for: .normal) 44 | button.setTitleColor(.white, for: .normal) 45 | button.backgroundColor = .black 46 | button.layer.cornerRadius = 5 47 | button.translatesAutoresizingMaskIntoConstraints = false 48 | button.addTarget(self, action: #selector(retrieveTapped), for: .touchUpInside) 49 | return button 50 | }() 51 | 52 | private let previewTextView: UITextView = { 53 | let textView = UITextView(frame: .zero) 54 | textView.isUserInteractionEnabled = false 55 | textView.layer.cornerRadius = 5 56 | textView.layer.borderWidth = 1 57 | textView.layer.borderColor = UIColor.black.cgColor 58 | textView.translatesAutoresizingMaskIntoConstraints = false 59 | return textView 60 | }() 61 | 62 | override func viewDidLoad() { 63 | super.viewDidLoad() 64 | 65 | cardView.creditCardDataDelegate = inputsView 66 | inputsView.creditCardDataDelegate = cardView 67 | 68 | [ 69 | cardView, 70 | inputsView, 71 | retrieveButton, 72 | previewTextView, 73 | ].forEach(view.addSubview) 74 | 75 | NSLayoutConstraint.activate([ 76 | cardView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100), 77 | cardView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), 78 | cardView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), 79 | 80 | inputsView.topAnchor.constraint(equalTo: cardView.bottomAnchor, constant: 24), 81 | inputsView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 16), 82 | inputsView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -16), 83 | 84 | retrieveButton.heightAnchor.constraint(equalToConstant: 44), 85 | retrieveButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), 86 | retrieveButton.topAnchor.constraint(equalTo: inputsView.bottomAnchor, constant: 24), 87 | 88 | previewTextView.centerXAnchor.constraint(equalTo: view.centerXAnchor), 89 | previewTextView.topAnchor.constraint(equalTo: retrieveButton.bottomAnchor, constant: 32), 90 | previewTextView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32), 91 | previewTextView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 32), 92 | previewTextView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -32), 93 | ]) 94 | } 95 | 96 | override func viewDidAppear(_ animated: Bool) { 97 | super.viewDidAppear(animated) 98 | cardView.currentInput = .cardNumber 99 | } 100 | 101 | @objc private func retrieveTapped() { 102 | let data = cardView.creditCardData 103 | previewTextView.text = 104 | """ 105 | \(data.cardProvider?.name ?? String()) 106 | \(data.cardNumber) 107 | \(data.cardholderName) 108 | \(data.validityDate) 109 | \(data.CVVNumber) 110 | """ 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '11.0' 2 | use_frameworks! 3 | 4 | target 'AnimatedCardInput_Example' do 5 | pod 'AnimatedCardInput', :path => '../' 6 | 7 | target 'AnimatedCardInput_Tests' do 8 | inherit! :search_paths 9 | 10 | 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AnimatedCardInput (0.1.0) 3 | 4 | DEPENDENCIES: 5 | - AnimatedCardInput (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | AnimatedCardInput: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | AnimatedCardInput: 7690e211754c9933c0bbf783f6a68688210560de 13 | 14 | PODFILE CHECKSUM: 8975e7ac366a4532f3ec06628a45c9b29bb59d6f 15 | 16 | COCOAPODS: 1.9.0 17 | -------------------------------------------------------------------------------- /Example/Pods/Local Podspecs/AnimatedCardInput.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AnimatedCardInput", 3 | "version": "0.1.0", 4 | "summary": "iOS library with components for input of Credit Card data", 5 | "description": "This library let's you drop into your project an animated component representing Credit Card and allow users to enter their data directly onto it.", 6 | "homepage": "https://github.com/krysztalzg/AnimatedCardInput", 7 | "license": { 8 | "type": "MIT", 9 | "file": "LICENSE" 10 | }, 11 | "authors": { 12 | "Kamil Szczepański": "kamil.szczepanski@netguru.com" 13 | }, 14 | "source": { 15 | "git": "https://github.com/krysztalzg/AnimatedCardInput.git", 16 | "tag": "0.1.0" 17 | }, 18 | "social_media_url": "https://www.netguru.com/codestories/topic/ios", 19 | "swift_versions": "5.0", 20 | "platforms": { 21 | "ios": "11.0" 22 | }, 23 | "source_files": "AnimatedCardInput/Classes/**/*", 24 | "resource_bundles": { 25 | "AnimatedCardInput": [ 26 | "AnimatedCardInput/Assets/*.png" 27 | ] 28 | }, 29 | "swift_version": "5.0" 30 | } 31 | -------------------------------------------------------------------------------- /Example/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AnimatedCardInput (0.1.0) 3 | 4 | DEPENDENCIES: 5 | - AnimatedCardInput (from `../`) 6 | 7 | EXTERNAL SOURCES: 8 | AnimatedCardInput: 9 | :path: "../" 10 | 11 | SPEC CHECKSUMS: 12 | AnimatedCardInput: 7690e211754c9933c0bbf783f6a68688210560de 13 | 14 | PODFILE CHECKSUM: 8975e7ac366a4532f3ec06628a45c9b29bb59d6f 15 | 16 | COCOAPODS: 1.9.0 17 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /Example/Pods/Pods.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/AnimatedCardInput/AnimatedCardInput-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/AnimatedCardInput/AnimatedCardInput-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_AnimatedCardInput : NSObject 3 | @end 4 | @implementation PodsDummy_AnimatedCardInput 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/AnimatedCardInput/AnimatedCardInput-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/AnimatedCardInput/AnimatedCardInput-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double AnimatedCardInputVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char AnimatedCardInputVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/AnimatedCardInput/AnimatedCardInput.debug.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 11 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/AnimatedCardInput/AnimatedCardInput.modulemap: -------------------------------------------------------------------------------- 1 | framework module AnimatedCardInput { 2 | umbrella header "AnimatedCardInput-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/AnimatedCardInput/AnimatedCardInput.release.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 11 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/AnimatedCardInput/ResourceBundle-AnimatedCardInput-AnimatedCardInput-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleIdentifier 8 | ${PRODUCT_BUNDLE_IDENTIFIER} 9 | CFBundleInfoDictionaryVersion 10 | 6.0 11 | CFBundleName 12 | ${PRODUCT_NAME} 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 0.1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## AnimatedCardInput 5 | 6 | Copyright (c) 2020 Netguru 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | /Users/krysztal/Documents/Xcode/podlibtest/AnimatedCardInput/AnimatedCardInput/Classes/ReplaceMe.swift 26 | 27 | Generated by CocoaPods - https://cocoapods.org 28 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2020 Netguru <hello@netguru.com> 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy 20 | of this software and associated documentation files (the "Software"), to deal 21 | in the Software without restriction, including without limitation the rights 22 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 23 | copies of the Software, and to permit persons to whom the Software is 24 | furnished to do so, subject to the following conditions: 25 | 26 | The above copyright notice and this permission notice shall be included in 27 | all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 35 | THE SOFTWARE. 36 | /Users/krysztal/Documents/Xcode/podlibtest/AnimatedCardInput/AnimatedCardInput/Classes/ReplaceMe.swift 37 | 38 | License 39 | MIT 40 | Title 41 | AnimatedCardInput 42 | Type 43 | PSGroupSpecifier 44 | 45 | 46 | FooterText 47 | Generated by CocoaPods - https://cocoapods.org 48 | Title 49 | 50 | Type 51 | PSGroupSpecifier 52 | 53 | 54 | StringsTable 55 | Acknowledgements 56 | Title 57 | Acknowledgements 58 | 59 | 60 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_AnimatedCardInput_Example : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_AnimatedCardInput_Example 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | warn_missing_arch=${2:-true} 88 | if [ -r "$source" ]; then 89 | # Copy the dSYM into the targets temp dir. 90 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 91 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 92 | 93 | local basename 94 | basename="$(basename -s .dSYM "$source")" 95 | binary_name="$(ls "$source/Contents/Resources/DWARF")" 96 | binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" 97 | 98 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 99 | if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then 100 | strip_invalid_archs "$binary" "$warn_missing_arch" 101 | fi 102 | 103 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 104 | # Move the stripped file into its final destination. 105 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 106 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 107 | else 108 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 109 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" 110 | fi 111 | fi 112 | } 113 | 114 | # Copies the bcsymbolmap files of a vendored framework 115 | install_bcsymbolmap() { 116 | local bcsymbolmap_path="$1" 117 | local destination="${BUILT_PRODUCTS_DIR}" 118 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" 119 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" 120 | } 121 | 122 | # Signs a framework with the provided identity 123 | code_sign_if_enabled() { 124 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 125 | # Use the current code_sign_identity 126 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 127 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 128 | 129 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 130 | code_sign_cmd="$code_sign_cmd &" 131 | fi 132 | echo "$code_sign_cmd" 133 | eval "$code_sign_cmd" 134 | fi 135 | } 136 | 137 | # Strip invalid architectures 138 | strip_invalid_archs() { 139 | binary="$1" 140 | warn_missing_arch=${2:-true} 141 | # Get architectures for current target binary 142 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 143 | # Intersect them with the architectures we are building for 144 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 145 | # If there are no archs supported by this binary then warn the user 146 | if [[ -z "$intersected_archs" ]]; then 147 | if [[ "$warn_missing_arch" == "true" ]]; then 148 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 149 | fi 150 | STRIP_BINARY_RETVAL=0 151 | return 152 | fi 153 | stripped="" 154 | for arch in $binary_archs; do 155 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 156 | # Strip non-valid architectures in-place 157 | lipo -remove "$arch" -output "$binary" "$binary" 158 | stripped="$stripped $arch" 159 | fi 160 | done 161 | if [[ "$stripped" ]]; then 162 | echo "Stripped $binary of architectures:$stripped" 163 | fi 164 | STRIP_BINARY_RETVAL=1 165 | } 166 | 167 | install_artifact() { 168 | artifact="$1" 169 | base="$(basename "$artifact")" 170 | case $base in 171 | *.framework) 172 | install_framework "$artifact" 173 | ;; 174 | *.dSYM) 175 | # Suppress arch warnings since XCFrameworks will include many dSYM files 176 | install_dsym "$artifact" "false" 177 | ;; 178 | *.bcsymbolmap) 179 | install_bcsymbolmap "$artifact" 180 | ;; 181 | *) 182 | echo "error: Unrecognized artifact "$artifact"" 183 | ;; 184 | esac 185 | } 186 | 187 | copy_artifacts() { 188 | file_list="$1" 189 | while read artifact; do 190 | install_artifact "$artifact" 191 | done <$file_list 192 | } 193 | 194 | ARTIFACT_LIST_FILE="${BUILT_PRODUCTS_DIR}/cocoapods-artifacts-${CONFIGURATION}.txt" 195 | if [ -r "${ARTIFACT_LIST_FILE}" ]; then 196 | copy_artifacts "${ARTIFACT_LIST_FILE}" 197 | fi 198 | 199 | if [[ "$CONFIGURATION" == "Debug" ]]; then 200 | install_framework "${BUILT_PRODUCTS_DIR}/AnimatedCardInput/AnimatedCardInput.framework" 201 | fi 202 | if [[ "$CONFIGURATION" == "Release" ]]; then 203 | install_framework "${BUILT_PRODUCTS_DIR}/AnimatedCardInput/AnimatedCardInput.framework" 204 | fi 205 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 206 | wait 207 | fi 208 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_AnimatedCardInput_ExampleVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_AnimatedCardInput_ExampleVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput/AnimatedCardInput.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "AnimatedCardInput" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_AnimatedCardInput_Example { 2 | umbrella header "Pods-AnimatedCardInput_Example-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Example/Pods-AnimatedCardInput_Example.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput/AnimatedCardInput.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "AnimatedCardInput" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 13 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | Generated by CocoaPods - https://cocoapods.org 4 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Generated by CocoaPods - https://cocoapods.org 18 | Title 19 | 20 | Type 21 | PSGroupSpecifier 22 | 23 | 24 | StringsTable 25 | Acknowledgements 26 | Title 27 | Acknowledgements 28 | 29 | 30 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_AnimatedCardInput_Tests : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_AnimatedCardInput_Tests 5 | @end 6 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_AnimatedCardInput_TestsVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_AnimatedCardInput_TestsVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests.debug.xcconfig: -------------------------------------------------------------------------------- 1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput" 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput/AnimatedCardInput.framework/Headers" 4 | OTHER_LDFLAGS = $(inherited) -framework "AnimatedCardInput" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 8 | PODS_ROOT = ${SRCROOT}/Pods 9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 10 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_AnimatedCardInput_Tests { 2 | umbrella header "Pods-AnimatedCardInput_Tests-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Example/Pods/Target Support Files/Pods-AnimatedCardInput_Tests/Pods-AnimatedCardInput_Tests.release.xcconfig: -------------------------------------------------------------------------------- 1 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput" 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/AnimatedCardInput/AnimatedCardInput.framework/Headers" 4 | OTHER_LDFLAGS = $(inherited) -framework "AnimatedCardInput" 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 8 | PODS_ROOT = ${SRCROOT}/Pods 9 | USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES 10 | -------------------------------------------------------------------------------- /Example/Tests/CardViewInputDelegateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardViewInputDelegateTests.swift 3 | // AnimatedCardInput_Tests 4 | // 5 | 6 | import XCTest 7 | import AnimatedCardInput 8 | 9 | class CardViewInputDelegateTests: XCTestCase { 10 | 11 | var sut: CardView! 12 | var resourceBundle: Bundle? 13 | 14 | override func setUp() { 15 | super.setUp() 16 | sut = CardView() 17 | resourceBundle = { 18 | let frameworkBundle = Bundle(for: CardView.self) 19 | if let resourceBundlePath = frameworkBundle.path(forResource: "AnimatedCardInput", ofType: "bundle") { 20 | return Bundle(path: resourceBundlePath) 21 | } 22 | return frameworkBundle 23 | }() 24 | } 25 | 26 | func testUpdateCardProviderVisa() throws { 27 | sut.updateCardProvider(cardNumber: "4") 28 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "visa.png", in: resourceBundle, compatibleWith: nil)) 29 | } 30 | 31 | func testUpdateCardProviderMastercard() throws { 32 | sut.updateCardProvider(cardNumber: "5") 33 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "mastercard.png", in: resourceBundle, compatibleWith: nil)) 34 | } 35 | 36 | func testUpdateCardProviderDiscover() throws { 37 | sut.updateCardProvider(cardNumber: "6") 38 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "discover.png", in: resourceBundle, compatibleWith: nil)) 39 | } 40 | 41 | func testUpdateCardProviderAmericanExpress4() throws { 42 | sut.updateCardProvider(cardNumber: "34") 43 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "american_express.png", in: resourceBundle, compatibleWith: nil)) 44 | } 45 | 46 | func testUpdateCardProviderAmericanExpress7() throws { 47 | sut.updateCardProvider(cardNumber: "37") 48 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "american_express.png", in: resourceBundle, compatibleWith: nil)) 49 | } 50 | 51 | func testUpdateCardProviderDiners0() throws { 52 | sut.updateCardProvider(cardNumber: "30") 53 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "diners_club.png", in: resourceBundle, compatibleWith: nil)) 54 | } 55 | 56 | func testUpdateCardProviderDiners6() throws { 57 | sut.updateCardProvider(cardNumber: "36") 58 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "diners_club.png", in: resourceBundle, compatibleWith: nil)) 59 | } 60 | 61 | func testUpdateCardProviderDiners8() throws { 62 | sut.updateCardProvider(cardNumber: "38") 63 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "diners_club.png", in: resourceBundle, compatibleWith: nil)) 64 | } 65 | 66 | func testUpdateCardProviderJCB() throws { 67 | sut.updateCardProvider(cardNumber: "35") 68 | XCTAssertEqual(sut.cardProviderImage, UIImage(named: "jcb.png", in: resourceBundle, compatibleWith: nil)) 69 | } 70 | 71 | func testUpdateCardProviderVisaNotSupported() throws { 72 | sut.updateCardProvider(cardNumber: "9") 73 | XCTAssertEqual(sut.cardProviderImage, nil) 74 | } 75 | 76 | func testUpdateCardProviderVisaNotSupported3() throws { 77 | sut.updateCardProvider(cardNumber: "39") 78 | XCTAssertEqual(sut.cardProviderImage, nil) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Example/Tests/CreditCardDataDelegateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CreditCardDataDelegateTests.swift 3 | // AnimatedCardInput_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import AnimatedCardInput 8 | 9 | class CreditCardDataDelegateTests: XCTestCase { 10 | 11 | var sut: CardView! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | sut = CardView() 16 | } 17 | 18 | func testCardNumberChanged() throws { 19 | sut.cardNumberChanged("newnumber") 20 | XCTAssertEqual(sut.creditCardData.cardNumber, "newnumber") 21 | } 22 | 23 | func testCardNumberChangedEmptyString() throws { 24 | sut.cardNumberChanged("") 25 | XCTAssertEqual(sut.creditCardData.cardNumber, "") 26 | } 27 | 28 | func testCardholderNameChanged() throws { 29 | sut.cardholderNameChanged("new carholder name") 30 | XCTAssertEqual(sut.creditCardData.cardholderName, "new carholder name".uppercased()) 31 | } 32 | 33 | func testCardholderNameChangedEmptyString() throws { 34 | sut.cardholderNameChanged("") 35 | XCTAssertEqual(sut.creditCardData.cardholderName, "") 36 | } 37 | 38 | func testCValidityDateChanged() throws { 39 | sut.validityDateChanged("1124") 40 | XCTAssertEqual(sut.creditCardData.validityDate, "11/24") 41 | } 42 | 43 | func testCValidityDateChangedEmptyString() throws { 44 | sut.validityDateChanged("") 45 | XCTAssertEqual(sut.creditCardData.validityDate, "") 46 | } 47 | 48 | func testCValidityDateChangedHalfString() throws { 49 | sut.validityDateChanged("11") 50 | XCTAssertEqual(sut.creditCardData.validityDate, "11") 51 | } 52 | 53 | func testCValidityDateChangedQuarterString() throws { 54 | sut.validityDateChanged("113") 55 | XCTAssertEqual(sut.creditCardData.validityDate, "11/3") 56 | } 57 | 58 | func testCVVNumberChanged() throws { 59 | sut.CVVNumberChanged("123") 60 | XCTAssertEqual(sut.creditCardData.CVVNumber, "123") 61 | } 62 | 63 | func testCVVNumberChangedEmptyString() throws { 64 | sut.CVVNumberChanged("") 65 | XCTAssertEqual(sut.creditCardData.CVVNumber, "") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Example/Tests/CreditCardDataProviderTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import AnimatedCardInput 3 | 4 | class CreditCardDataProvider: XCTestCase { 5 | 6 | func testRetrievingCreditCardDataFromCardView() { 7 | let sut = CardView() 8 | XCTAssertNotNil(sut.creditCardData) 9 | } 10 | 11 | func testRetrievingCreditCardDataFromCardInputsView() { 12 | let sut = CardInputsView(cardNumberDigitLimit: 1) 13 | XCTAssertNotNil(sut.creditCardData) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Example/Tests/CustomInputFieldTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomInputFieldTests.swift 3 | // AnimatedCardInput_Tests 4 | // 5 | 6 | import XCTest 7 | @testable import AnimatedCardInput 8 | 9 | class CustomInputFieldTests: XCTestCase { 10 | 11 | var sut: CustomInputField! 12 | 13 | override func setUp() { 14 | super.setUp() 15 | sut = CustomInputField() 16 | } 17 | 18 | func testInputHasChanged() throws { 19 | sut.inputHasChanged(to: "newInput") 20 | XCTAssertEqual(sut.text, "newInput") 21 | } 22 | 23 | func testInputHasChangedEmptyString() throws { 24 | sut.inputHasChanged(to: "") 25 | XCTAssertEqual(sut.text, "") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Example/Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Netguru 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | /Users/krysztal/Documents/Xcode/podlibtest/AnimatedCardInput/AnimatedCardInput/Classes/ReplaceMe.swift 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AnimatedCardInput 2 | 3 | [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://raw.githubusercontent.com/krysztalzg/AnimatedCardInput/master/LICENSE) 4 | [![CocoaPods](https://img.shields.io/cocoapods/v/AnimatedCardInput.svg)](http://cocoapods.org/pods/AnimatedCardInput) 5 | [![Swift 5.0](https://img.shields.io/badge/Swift-5.0-green.svg)](https://developer.apple.com/swift/) 6 | 7 | This library allows you to drop into your project two easily customisable, animated components that will make input of Credit Card information for users much better experience. 8 |

9 | 10 |

11 | 12 | ## Example 13 | 14 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 15 | 16 | ## Requirements 17 | 18 | *AnimatedCardInput* is written in **Swift 5.0** and supports **iOS 11.0+**. 19 | 20 | ## Installation 21 | 22 | *AnimatedCardInput* is available through [CocoaPods](https://cocoapods.org). To install 23 | it, simply add the following line to your Podfile: 24 | 25 | ```ruby 26 | pod 'AnimatedCardInput' 27 | ``` 28 | 29 | ## Usage 30 | 31 | Both components available in this Pod can be used either individually or in pair on one screen. 32 | 33 | ### Card View 34 | ![cardViewBothsides](https://user-images.githubusercontent.com/6009785/82155437-4e459500-9875-11ea-8242-a09f83f7af52.png) 35 | To use Card View on our screen we simply initialise the view and place it in our hierarchy. This will create default credit card view, with 16-digit card number separated into blocks of 4 and CVV number limited to 3 digits. 36 | ```swift 37 | /// cardNumberDigitsLimit: Indicates maximum length of card number. Defaults to 16. 38 | /// cardNumberChunkLengths: Indicates format of card number, 39 | /// e.g. [4, 3] means that number of length 7 will be split 40 | /// into two parts of length 4 and 3 respectively (XXXX XXX). 41 | /// CVVNumberDigitsLimit: Indicates maximum length of CVV number. 42 | let cardView = CardView( 43 | cardNumberDigitsLimit: 16, 44 | cardNumberChunkLengths: [4, 4, 4, 4], 45 | CVVNumberDigitsLimit: 3 46 | ) 47 | view.addSubview(cardView) 48 | NSLayoutConstraint.activate([...]) 49 | ``` 50 | We can customise our card view in almost every way. Starting with the design, both fonts and color can be adjusted. Here's a list of all available properties that can be changed. 51 | 52 | * `isSecureInput` - Enables masking of CVV number (defaults to false). 53 | * `validatesDateInput` - Enables validation on Validity Date Field (defaults to true). 54 | * `frontSideCardColor` - Background color of the card's front side (defaults to #373737). 55 | * `frontSideTextColor` - Text color of the card's front side (defaults to #FFFFFF). 56 | * `backSideCardColor` - Background color of the card's back side (defaults to #373737). 57 | * `CVVBackgroundColor` - Background color of the card's CVV Field (defaults to #FFFFFF). 58 | * `backSideTextColor` - Text color of the card's back side (defaults to #000000). 59 | * `selectionIndicatorColor` - Border color of a selected field indicator (defaults to #ff8000). 60 | * `numberInputFont` - Font of the Card Number Field (defaults to System SemiBold 24). 61 | * `nameLabelFont` - Font of the Cardholder Name Label (defaults to System Light 14. Recommended font size is 0.6 of Card Number size). 62 | * `nameInputFont` - Font of the Cardholder Name Field (defaults to System Regular 14. Recommended font size is 0.6 of Card Number size). 63 | * `validityLabelFont` - Font of the Validity Date Label (defaults to System Light 14. Recommended font size is 0.6 of Card Number size). 64 | * `validityInputFont` - Font of the Validity Date Field (defaults to System Regular 14. Recommended font size is 0.6 of Card Number size). 65 | * `CVVInputFont` - Font of the CVV Number Field (defaults to System SemiBold 20. Recommended font size is 0.85 of Card Number size). 66 | * `cardNumberSeparator` - Character used as the Card Number Separator (defaults to " "). 67 | * `cardNumberEmptyCharacter` - Character used as the Card Number Empty Character (defaults to "X"). 68 | * `validityDateSeparator` - Character used as the Validity Date Separator (defaults to "/"). 69 | * `validityDateCustomPlaceHolder` - Text used as the Validity Date Placeholder (defaults to "MM/YY"). 70 | * `CVVNumberEmptyCharacter` - Character used as CVV Number Empty Character (defaults to "X"). 71 | * `cardholderNameTitle` - Custom string to use for title label of Cardholder Name input. 72 | * `cardholderNamePlaceholder` - Custom string to use for placeholder of Cardholder Name input. 73 | * `validityDateTitle` - Custom string to use for title label of Validity Date input. 74 | 75 | ### Card Inputs View 76 | ![CardInputsViewCardholderName](https://user-images.githubusercontent.com/6009785/82155441-500f5880-9875-11ea-85f4-95920ff23e3d.png) 77 | To use Card Inputs View on our screen we simply initialise the view and place it in our hierarchy. This will create default default scroll view with four text fields and card number limited to 16 digits. 78 | ```swift 79 | let cardInputsView = CardInputsView(cardNumberDigitLimit: 16) 80 | view.addSubview(cardInputsView) 81 | NSLayoutConstraint.activate([...]) 82 | ``` 83 | Input views can be customised all at once with following properties. 84 | * `isSecureInput` - Enables masking ov CVV number (defaults to false). 85 | * `validatesDateInput` - Enables validation on Validity Date Field (defaults to true). 86 | * `inputTintColor` - Color of tint for text fields (defaults to #000000). 87 | * `inputborderColor` - Color of border for text fields (defaults to #808080). 88 | * `titleColor` - Color of text in title label (defaults to #000000). 89 | * `titleFont` - Font of text in title label (defaults to System Light 12). 90 | * `inputColor` - Color of text in text field (defaults to #000000). 91 | * `inputFont` - Font of text in text field (defaults to System Regular 24). 92 | * `cardNumberTitle` - Custom string to use for title label of Card Number input. 93 | * `cardholderNameTitle` - Custom string to use for title label of Cardholder Name input. 94 | * `validityDateTitle` - Custom string to use for title label of Validity Date input. 95 | * `cvvNumberTitle` - Custom string to use for title label of CVV Code input. 96 | 97 | ### Connecting both components 98 | If we want users to input data either directly onto card or by selecting textfields we need to pair both components with `creditCardDataDelegate` property. 99 | ```swift 100 | cardView.creditCardDataDelegate = inputsView 101 | cardInputsView.creditCardDataDelegate = cardView 102 | ``` 103 | From now on both inputs will update to the same data and step in the filling flow. 104 | 105 | ### Adding new Card Providers 106 | If default card brands provided with this library are not enough for your needs, you can add your own Card Provider with custom name, icon and recognition pattern that will be used in regex (`"^\(pattern)\\d*"), eg. a fake _Cardinio_ provider that have card numbers starting with `92` or `95`: 107 | ```swift 108 | let newCardProvider = CardProvider(name: "Cardinio", image: #imageLiteral(resourceName: "cardinio_icon"), pattern: "9[2,5]") 109 | CardProvider.addCardProviders(newCardProvider) 110 | ``` 111 | 112 | ## Roadmap 113 | * [x] Labels localisation. 114 | * [x] Adding new, custom credit card providers with icons. 115 | * [ ] Validation errors displayed on the inputs. 116 | * [ ] Even more... 117 | 118 | ## About 119 | 120 | This project is made with ❤️ by [Netguru](https://netguru.com) and maintained by [Kamil Szczepański](https://github.com/krysztalzg). 121 | 122 | ## License 123 | 124 | *AnimatedCardInput* is available under the MIT license. See the LICENSE file for more info. 125 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj --------------------------------------------------------------------------------