diff --git a/CardReader.xcodeproj/project.pbxproj b/CardReader.xcodeproj/project.pbxproj index b7871bb..b5a7227 100644 --- a/CardReader.xcodeproj/project.pbxproj +++ b/CardReader.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 19B45F61264B8A5C00B33163 /* Color+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B45F60264B8A5C00B33163 /* Color+Extensions.swift */; }; 19B45F69264B919400B33163 /* CreditCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B45F68264B919400B33163 /* CreditCardView.swift */; }; + 19B45F82264CCA3A00B33163 /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B45F81264CCA3A00B33163 /* UIApplication+Extensions.swift */; }; 19DC60D02643524C00E29843 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DC60CF2643524C00E29843 /* AppDelegate.swift */; }; 19DC60D22643524C00E29843 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19DC60D12643524C00E29843 /* SceneDelegate.swift */; }; 19DC60D92643524D00E29843 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 19DC60D82643524D00E29843 /* Assets.xcassets */; }; @@ -42,6 +43,7 @@ /* Begin PBXFileReference section */ 19B45F60264B8A5C00B33163 /* Color+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extensions.swift"; sourceTree = ""; }; 19B45F68264B919400B33163 /* CreditCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardView.swift; sourceTree = ""; }; + 19B45F81264CCA3A00B33163 /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; 19DC60CC2643524C00E29843 /* CardReader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CardReader.app; sourceTree = BUILT_PRODUCTS_DIR; }; 19DC60CF2643524C00E29843 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 19DC60D12643524C00E29843 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -90,6 +92,7 @@ isa = PBXGroup; children = ( 19B45F60264B8A5C00B33163 /* Color+Extensions.swift */, + 19B45F81264CCA3A00B33163 /* UIApplication+Extensions.swift */, ); path = Extensions; sourceTree = ""; @@ -320,6 +323,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 19B45F82264CCA3A00B33163 /* UIApplication+Extensions.swift in Sources */, 19DC62A82644EAA600E29843 /* CardFormView.swift in Sources */, 19DC610B2643527200E29843 /* CardReaderView.swift in Sources */, 19DC60D02643524C00E29843 /* AppDelegate.swift in Sources */, diff --git a/CardReader.xcodeproj/project.xcworkspace/xcuserdata/khalidasad.xcuserdatad/UserInterfaceState.xcuserstate b/CardReader.xcodeproj/project.xcworkspace/xcuserdata/khalidasad.xcuserdatad/UserInterfaceState.xcuserstate index 2176b38..cefede4 100644 Binary files a/CardReader.xcodeproj/project.xcworkspace/xcuserdata/khalidasad.xcuserdatad/UserInterfaceState.xcuserstate and b/CardReader.xcodeproj/project.xcworkspace/xcuserdata/khalidasad.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/CardReader/Assets.xcassets/scan.imageset/Contents.json b/CardReader/Assets.xcassets/scan.imageset/Contents.json new file mode 100644 index 0000000..fabe15d --- /dev/null +++ b/CardReader/Assets.xcassets/scan.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "scan-icon-15.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/CardReader/Assets.xcassets/scan.imageset/scan-icon-15.png b/CardReader/Assets.xcassets/scan.imageset/scan-icon-15.png new file mode 100644 index 0000000..ddc8bd4 Binary files /dev/null and b/CardReader/Assets.xcassets/scan.imageset/scan-icon-15.png differ diff --git a/CardReader/Extensions/Color+Extensions.swift b/CardReader/Extensions/Color+Extensions.swift index e0e126d..6e6ef0a 100644 --- a/CardReader/Extensions/Color+Extensions.swift +++ b/CardReader/Extensions/Color+Extensions.swift @@ -8,6 +8,13 @@ import Foundation import SwiftUI +public extension UIColor { + + static var darkGrayColor: UIColor { + UIColor(red: 40.0/255.0, green: 40.0/255.0, blue: 40.0/255.0, alpha: 1.0) + } +} + public extension Color { static var isDarkInterfaceStyle: Bool { @@ -19,7 +26,7 @@ public extension Color { } static var backgroundColor: Color { - Color(UIColor { $0.userInterfaceStyle == .dark ? .darkGray : .white }) + Color(UIColor { $0.userInterfaceStyle == .dark ? .darkGrayColor : .white }) } static var grayColor: Color { diff --git a/CardReader/Extensions/UIApplication+Extensions.swift b/CardReader/Extensions/UIApplication+Extensions.swift new file mode 100644 index 0000000..79e2423 --- /dev/null +++ b/CardReader/Extensions/UIApplication+Extensions.swift @@ -0,0 +1,16 @@ +// +// UIApplication+Extensions.swift +// CardReader +// +// Created by Khalid Asad on 2021-05-12. +// + +import Foundation +import UIKit + +extension UIApplication { + + func endEditing() { + sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } +} diff --git a/CardReader/Models/CardDetails.swift b/CardReader/Models/CardDetails.swift index 1509ba2..319a83c 100644 --- a/CardReader/Models/CardDetails.swift +++ b/CardReader/Models/CardDetails.swift @@ -10,15 +10,17 @@ import SwiftUI public struct CardDetails: Hashable, Identifiable { public var number: String? - public var expiryDate: String? public var name: String? + public var expiryDate: String? + public var cvcNumber: String? public var type: CardType public var industry: CardIndustry - public init(numberWithDelimiters: String? = nil, expiryDate: String? = nil, name: String? = nil) { + public init(numberWithDelimiters: String? = nil, name: String? = nil, expiryDate: String? = nil, cvcNumber: String? = nil) { self.number = numberWithDelimiters - self.expiryDate = expiryDate self.name = name + self.expiryDate = expiryDate + self.cvcNumber = cvcNumber self.type = CardType(number: numberWithDelimiters?.replacingOccurrences(of: " ", with: "")) self.industry = .init(firstDigit: numberWithDelimiters?.first) } diff --git a/CardReader/Models/ImageTextRecognizable.swift b/CardReader/Models/ImageTextRecognizable.swift index d56c55e..ddc6617 100644 --- a/CardReader/Models/ImageTextRecognizable.swift +++ b/CardReader/Models/ImageTextRecognizable.swift @@ -59,6 +59,6 @@ public extension ImageTextRecognizable { CardType.allCases.map { $0.rawValue.uppercased() } let name = recognizedText.filter({ !wordsToAvoid.contains($0) }).last - return CardDetails(numberWithDelimiters: creditCardNumber, expiryDate: expiryDate, name: name) + return CardDetails(numberWithDelimiters: creditCardNumber, name: name, expiryDate: expiryDate) } } diff --git a/CardReader/SceneDelegate.swift b/CardReader/SceneDelegate.swift index 105df34..aaee40a 100644 --- a/CardReader/SceneDelegate.swift +++ b/CardReader/SceneDelegate.swift @@ -14,7 +14,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let windowScene = (scene as? UIWindowScene) else { return } let window = UIWindow(windowScene: windowScene) - window.rootViewController = UIHostingController(rootView: CardFormView()) + window.rootViewController = UIHostingController(rootView: CardFormView(completion: { _ in })) self.window = window window.makeKeyAndVisible() } diff --git a/CardReader/Views/CardForm/CardFormField.swift b/CardReader/Views/CardForm/CardFormField.swift index df30b4c..8ef46de 100644 --- a/CardReader/Views/CardForm/CardFormField.swift +++ b/CardReader/Views/CardForm/CardFormField.swift @@ -22,7 +22,15 @@ public struct CardFormField: View { var isCreditCardNumber: Bool var isExpiryDate: Bool - init(fieldTitle: String, text: Binding, isSecure: Bool = false, autocapitalizationType: UITextAutocapitalizationType = .none, onEdit: (() -> Void)? = nil, isCreditCardNumber: Bool = false, isExpiryDate: Bool = false) { + init( + fieldTitle: String, + text: Binding, + isSecure: Bool = false, + autocapitalizationType: UITextAutocapitalizationType = .none, + onEdit: (() -> Void)? = nil, + isCreditCardNumber: Bool = false, + isExpiryDate: Bool = false + ) { self.fieldTitle = fieldTitle self._text = text self.isSecure = isSecure @@ -35,50 +43,45 @@ public struct CardFormField: View { public var body: some View { VStack(alignment: .leading) { Text(fieldTitle) - .font(.system(size: 16)) + .font(.system(size: 16, weight: .bold, design: .rounded)) .foregroundColor(isEditing ? .red : .primaryColor) - if isSecure { - SecureField("", text: $text, onCommit: { onEdit?() }) - .font(.system(size: 20)) - .foregroundColor(.black) - .autocapitalization(autocapitalizationType) - .disableAutocorrection(true) - .border(Color(UIColor.separator)) - .padding(.all, 4) - .background(Color.grayColor) - .cornerRadius(5) - } else { - TextField( - "", - text: $text, - onEditingChanged: { isEditing in - self.isEditing = isEditing - if !isEditing { onEdit?() } - }) - .font(.largeTitle) - .foregroundColor(.black) - .autocapitalization(autocapitalizationType) - .disableAutocorrection(true) - .padding(.all, 4) - .background(Color.grayColor) - .cornerRadius(5) -// .onReceive(Just(text), perform: { newValue in -// if isCreditCardNumber { -// if [3, 8, 13].contains(text.count) && [4, 9, 15].contains(newValue.count) { -// self.text = newValue + " " -// } else if [5, 0, 15].contains(newValue.count) && [6, 11, 16].contains(text.count) { -// self.text = String(newValue.dropLast(1)) + Group { + if isSecure { + SecureField("", text: $text, onCommit: { onEdit?() }) + } else { + TextField( + "", + text: $text, + onEditingChanged: { isEditing in + self.isEditing = isEditing + if !isEditing { onEdit?() } + } + ) +// .onReceive(Just(text), perform: { newValue in +// if isCreditCardNumber { +// if [3, 8, 13].contains(text.count) && [4, 9, 15].contains(newValue.count) { +// self.text = newValue + " " +// } else if [5, 0, 15].contains(newValue.count) && [6, 11, 16].contains(text.count) { +// self.text = String(newValue.dropLast(1)) +// } else { +// self.text = newValue +// } +// } else if isExpiryDate { +// self.text = newValue // } else { // self.text = newValue // } -// } else if isExpiryDate { -// self.text = newValue -// } else { -// self.text = newValue -// } -// }) +// }) + } } + .font(.system(size: 20, weight: .bold, design: .monospaced)) + .foregroundColor(.black) + .autocapitalization(autocapitalizationType) + .disableAutocorrection(true) + .padding(.all, 6) + .background(Color.grayColor) + .cornerRadius(5) } } } diff --git a/CardReader/Views/CardForm/CardFormView.swift b/CardReader/Views/CardForm/CardFormView.swift index 4faf5e7..f288be3 100644 --- a/CardReader/Views/CardForm/CardFormView.swift +++ b/CardReader/Views/CardForm/CardFormView.swift @@ -12,50 +12,99 @@ public struct CardFormView: View { @State private var isShowingSheet = false @State private var cardNumber: String = "" - @State private var cardExpiryDate: String = "" @State private var cardName: String = "" + @State private var cardExpiryDate: String = "" + @State private var cvcNumber: String = "" + + public var completion: ((CardDetails) -> Void) + private var colors: [Color] private var formattedCardNumber: String { cardNumber == "" ? "4111 2222 3333 4444" : cardNumber } private var cardIndustry: CardIndustry { .init(firstDigit: formattedCardNumber.first) } + public init(colors: [Color] = [.green, .blue, .black], completion: @escaping ((CardDetails) -> Void )) { + self.colors = colors + self.completion = completion + } + public var body: some View { ScrollView(.vertical) { VStack { - CreditCardView(backgroundColors: [.blue, .black], cardNumber: $cardNumber, cardExpiryDate: $cardExpiryDate, cardName: $cardName) + CreditCardView(backgroundColors: colors, cardNumber: $cardNumber, cardExpiryDate: $cardExpiryDate, cardName: $cardName) .shadow(color: .primaryColor, radius: 5) - .padding(.top, 30) - - Spacer(minLength: 30) - + .padding(.top, 60) + if cardIndustry != .unknown { Text(cardIndustry.rawValue) .font(.system(size: 14)) .foregroundColor(.primaryColor) + .padding(.top, 10) } - VStack(alignment: .leading) { - CardFormField(fieldTitle: "Card Number", text: $cardNumber, isCreditCardNumber: true) - .keyboardType(.numberPad) - - CardFormField(fieldTitle: "Card Expiry Date", text: $cardExpiryDate, isExpiryDate: true) - .keyboardType(.numberPad) - - CardFormField(fieldTitle: "Card Name", text: $cardName, autocapitalizationType: .words) - .keyboardType(.alphabet) - - Button { - isShowingSheet.toggle() - } label: { - Text("Scan card instead?") - .font(.title) - .fontWeight(.bold) - .multilineTextAlignment(.leading) + Button(action: { + isShowingSheet.toggle() + }) { + VStack(alignment: .center) { + Image("scan") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 60, height: 60) + + Text("Scan Card") + .font(.system(size: 26, weight: .bold, design: .monospaced)) + } + .foregroundColor(.primaryColor) + .padding(.all, 12) + .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.primaryColor, lineWidth: 1)) + } + .padding(.top, 30) + + VStack(alignment: .center) { + VStack(alignment: .leading, spacing: 10) { + CardFormField(fieldTitle: "Card Number", text: $cardNumber, isCreditCardNumber: true) + .keyboardType(.numberPad) + + CardFormField(fieldTitle: "Card Name", text: $cardName, autocapitalizationType: .words) + .keyboardType(.alphabet) + + HStack(spacing: 20) { + CardFormField(fieldTitle: "Card Expiry Date", text: $cardExpiryDate, isExpiryDate: true) + .keyboardType(.numberPad) + + CardFormField(fieldTitle: "CVC #", text: $cvcNumber) + .keyboardType(.numberPad) + } + } + + Button(action: { + let cardInfo = CardDetails( + numberWithDelimiters: cardNumber, + name: cardName, + expiryDate: cardExpiryDate, + cvcNumber: cvcNumber + ) + completion(cardInfo) + }) { + HStack(alignment: .center) { + Image(systemName: "bookmark.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 20, height: 20) + + Text("Save") + .font(.system(size: 26, weight: .bold, design: .monospaced)) + } + .foregroundColor(Color.white) + .padding(.all, 12) + .background(colors.first) + .cornerRadius(16) + .overlay(RoundedRectangle(cornerRadius: 16).stroke(Color.primaryColor, lineWidth: 1)) } - .padding(.top, 20) + .padding(.top, 26) } .sheet(isPresented: $isShowingSheet) { CardReaderView() { cardDetails in - print(cardDetails) + print(cardDetails ?? "") cardNumber = cardDetails?.number ?? "" cardExpiryDate = cardDetails?.expiryDate ?? "" cardName = cardDetails?.name ?? "" @@ -64,15 +113,20 @@ public struct CardFormView: View { .edgesIgnoringSafeArea(.all) } .padding(.horizontal, 15) - .padding(.top, 50) + .padding(.top, 10) } } + .onTapGesture { + UIApplication.shared.endEditing() + } + .background(Color.backgroundColor) + .edgesIgnoringSafeArea(.all) } } struct MainView_Previews: PreviewProvider { static var previews: some View { - CardFormView() + CardFormView(completion: { _ in }) } } diff --git a/CardReader/Views/CreditCard/CreditCardView.swift b/CardReader/Views/CreditCard/CreditCardView.swift index 77c5fed..1c7c26c 100644 --- a/CardReader/Views/CreditCard/CreditCardView.swift +++ b/CardReader/Views/CreditCard/CreditCardView.swift @@ -48,28 +48,27 @@ public struct CreditCardView: View { .frame(width: 60, height: 60) Text(formattedCardNumber) - .font(.system(size: 26)) - .bold() + .font(.system(size: 26, weight: .bold, design: .monospaced)) Spacer() HStack(alignment: .center) { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 4) { Text("Name") - .font(.system(size: 14)) - + .font(.system(size: 14, weight: .regular, design: .rounded)) + Text(formattedCardName) - .bold() + .font(.system(size: 16, weight: .bold, design: .monospaced)) } Spacer(minLength: 10) - VStack(alignment: .trailing) { + VStack(alignment: .trailing, spacing: 4) { Text("Exp. Date") - .font(.system(size: 14)) - + .font(.system(size: 14, weight: .regular, design: .rounded)) + Text(formattedCardExpiryDate) - .bold() + .font(.system(size: 16, weight: .bold, design: .monospaced)) } Spacer(minLength: 10) @@ -78,12 +77,11 @@ public struct CreditCardView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 60, height: 60) - .padding(.top) } } - .padding() + .padding(.init(top: 8, leading: 16, bottom: 4, trailing: 16)) .foregroundColor(textColor) - .background(LinearGradient(gradient: Gradient(colors: backgroundColors), startPoint: .leading, endPoint: .trailing)) + .background(LinearGradient(gradient: Gradient(colors: backgroundColors), startPoint: .topLeading, endPoint: .bottomTrailing)) .cornerRadius(7) .frame(width: 340, height: 200, alignment: .center) } diff --git a/README.md b/README.md index 49734b2..9b5aaa5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # card-reader-ios A credit card reader and parser for iOS Using Native Vision/VisionKit -https://user-images.githubusercontent.com/37077623/118050836-06553300-b34e-11eb-922e-94dcf6f8d905.mp4 +https://user-images.githubusercontent.com/37077623/118071540-f94c3a00-b375-11eb-97cc-35326e19499b.mp4 # Instructions - Hold camera up to a card and stay still until it gets recoginized and a picture gets taken. @@ -13,7 +13,14 @@ There are 2 options to present: 1. Simply navigate to or present CardResultsView as a sheet or View: `var body: some View { - CardFormView() + CardFormView(colors: [.green, .blue, .black], completion: { cardDetails in + print("Card Number:\n\(cardDetails.number ?? "")") + print("Expiry Date:\n\(cardDetails.expiryDate ?? "")") + print("Name:\n\(cardDetails.name ?? "")") + print("Name:\n\(cardDetails.cvcNumber ?? "")") + print("Card Type:\n\(cardDetails.type.rawValue)") + print("Card Industry:\n\(cardDetails.industry.rawValue)") + }) }`