Issue #346
We have FrontCard that contains number and expiration date, BackCard that contains CVC. CardView is used to contain front and back sides for flipping transition.
We leverage STPPaymentCardTextField
from Stripe for working input fields, then CardHandler
is used to parse STPPaymentCardTextField
content and update our UI.
For masked credit card numbers, we pad string to fit 16 characters with ●
symbol, then chunk into 4 parts and zip with labels to update.
For flipping animation, we use UIView.transition
with showHideTransitionViews
BackCard.swift
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import UIKitfinal class BackCard : UIView { lazy var rectangle: UIView = { let view = UIView () view.backgroundColor = R .color.darkText return view }() lazy var cvcLabel: UILabel = { let label = UILabel () label.font = R .customFont.medium(14 ) label.textColor = R .color.darkText label.textAlignment = .center return label }() lazy var cvcBox: UIView = { let view = UIView () view.backgroundColor = R .color.lightText return view }() override init (frame: CGRect ) { super .init (frame: frame) setup() } required init ?(coder aDecoder: NSCoder ) { fatalError () } private func setup () { addSubviews([rectangle, cvcBox, cvcLabel]) NSLayoutConstraint .on([ rectangle.leftAnchor.constraint(equalTo: leftAnchor), rectangle.rightAnchor.constraint(equalTo: rightAnchor), rectangle.heightAnchor.constraint(equalToConstant: 52 ), rectangle.topAnchor.constraint(equalTo: topAnchor, constant: 30 ), cvcBox.rightAnchor.constraint(equalTo: rightAnchor, constant: -16 ), cvcBox.topAnchor.constraint(equalTo: rectangle.bottomAnchor, constant: 16 ), cvcBox.widthAnchor.constraint(equalToConstant: 66 ), cvcBox.heightAnchor.constraint(equalToConstant: 30 ), cvcLabel.centerXAnchor.constraint(equalTo: cvcBox.centerXAnchor), cvcLabel.centerYAnchor.constraint(equalTo: cvcBox.centerYAnchor) ]) } }
FrontCard.swift
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 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 60 61 62 63 import UIKitfinal class FrontCard : UIView { lazy var stackView: UIStackView = { let stackView = UIStackView () stackView.axis = .horizontal stackView.distribution = .equalSpacing return stackView }() lazy var numberLabels: [UILabel ] = Array (0 ..<4 ).map ({ _ in return UILabel () }) lazy var expirationStaticLabel: UILabel = { let label = UILabel () label.font = R .customFont.regular(10 ) label.textColor = R .color.darkText return label }() lazy var expirationLabel: UILabel = { let label = UILabel () label.font = R .customFont.medium(14 ) label.textColor = R .color.darkText return label }() override init (frame: CGRect ) { super .init (frame: frame) setup() } required init ?(coder aDecoder: NSCoder ) { fatalError () } private func setup () { addSubview(stackView) numberLabels.forEach { stackView.addArrangedSubview($0 ) } addSubviews([expirationStaticLabel, expirationLabel]) numberLabels.forEach { $0 .font = R .customFont.medium(16 ) $0 .textColor = R .color.darkText $0 .textAlignment = .center } NSLayoutConstraint .on([ stackView.heightAnchor.constraint(equalToConstant: 50 ), stackView.leftAnchor.constraint(equalTo: leftAnchor, constant: 24 ), stackView.rightAnchor.constraint(equalTo: rightAnchor, constant: -24 ), stackView.topAnchor.constraint(equalTo: centerYAnchor), expirationStaticLabel.topAnchor.constraint(equalTo: stackView.bottomAnchor), expirationStaticLabel.leftAnchor.constraint(equalTo: rightAnchor, constant: -70 ), expirationLabel.leftAnchor.constraint(equalTo: expirationStaticLabel.leftAnchor), expirationLabel.topAnchor.constraint(equalTo: expirationStaticLabel.bottomAnchor) ]) } }
CardView.swift
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 import UIKitfinal class CardView : UIView { let backCard = BackCard () let frontCard = FrontCard () override init (frame: CGRect ) { super .init (frame: frame) setup() } required init ?(coder aDecoder: NSCoder ) { fatalError () } private func setup () { addSubview(backCard) addSubview(frontCard) [backCard, frontCard].forEach { NSLayoutConstraint .on([ $0 .pinEdges(view: self ) ]) $0 .clipsToBounds = true $0 .layer.cornerRadius = 10 $0 .backgroundColor = R .color.card.background } } }
CardHandler.swift
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 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 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 import Foundationimport Stripefinal class CardHandler { let cardView: CardView init (cardView: CardView ) { self .cardView = cardView } func reset () { cardView.frontCard.expirationStaticLabel.text = R .string.localizable.cardExpiration() cardView.frontCard.expirationLabel.text = R .string.localizable.cardExpirationPlaceholder() cardView.backCard.cvcLabel.text = R .string.localizable.cardCvcPlaceholder() } func showFront () { flip( from: cardView.backCard, to: cardView.frontCard, options: .transitionFlipFromLeft ) } func showBack () { flip( from: cardView.frontCard, to: cardView.backCard, options: .transitionFlipFromRight ) } func handle (_ textField: STPPaymentCardTextField) { handle(number: textField.cardNumber ?? "" ) handle(month: textField.formattedExpirationMonth, year: textField.formattedExpirationYear) handle(cvc: textField.cvc) } private func handle (number: String) { let paddedNumber = number.padding( toLength: 16 , withPad: R .string.localizable.cardNumberPlaceholder(), startingAt: 0 ) let chunkedNumbers = paddedNumber.chunk(by: 4 ) zip (cardView.frontCard.numberLabels, chunkedNumbers).forEach { tuple in tuple.0 .text = tuple.1 } } private func handle (cvc: String?) { if let cvc = cvc, !cvc.isEmpty { cardView.backCard.cvcLabel.text = cvc } else { cardView.backCard.cvcLabel.text = R .string.localizable.cardCvcPlaceholder() } } private func handle (month: String?, year: String?) { guard let month = month, let year = year, !month.isEmpty else { cardView.frontCard.expirationLabel.text = R .string.localizable.cardExpirationPlaceholder() return } let formattedYear = year.ifEmpty(replaceWith: "00" ) cardView.frontCard.expirationLabel.text = "\(month)/\(formattedYear)" } private func flip (from: UIView, to: UIView, options: UIView.AnimationOptions) { UIView .transition( from: from, to: to, duration: 0.25 , options: [options, .showHideTransitionViews], completion: nil ) } }
String+Extension.swift
1 2 3 4 5 6 7 8 9 10 11 12 13 extension String { func ifEmpty (replaceWith: String) -> String { return isEmpty ? replaceWith : self } func chunk (by length: Int) -> [String ] { return stride (from: 0 , to: count , by: length).map { let start = index(startIndex, offsetBy: $0 ) let end = index(start, offsetBy: length, limitedBy: endIndex) ?? endIndex return String (self [start..<end]) } } }
Updated at 2020-07-12 08:43:21