How to create a Credit Card Scanner in Swift

Work with image recognise can bring some goosebumps to developers… but I say to you:

Every year, the native framework Vision, introduced in iOS 11, have more features and is more accurate. In iOS 13 is too much smart…

To create a Credit Card Scanner you need to do 3 steps:
1. Implement a camera layer in your view;
2. Add a recognise area.
3. Use Vision to make an OCR.

The result:

In my Open Source library you can find all code because here I’ll show you how I recognise the text!

While I’m developing I had a problem: dimensions! The size of view vs the size of recognise area vs the size of recognised image vs the device size.

To solve this I created some transforms, to use only one size as base.

let ciImage = CIImage(cvImageBuffer: frame)
let widht = UIScreen.main.bounds.width - (UIScreen.main.bounds.width * 0.2)
let height = widht - (widht * 0.45)
let viewX = (UIScreen.main.bounds.width / 2) - (widht / 2)
let viewY = (UIScreen.main.bounds.height / 2) - (height / 2) - 100 + height

let resizeFilter = CIFilter(name: "CILanczosScaleTransform")!

// Desired output size
let targetSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

// Compute scale and corrective aspect ratio
let scale = targetSize.height / ciImage.extent.height
let aspectRatio = targetSize.width / (ciImage.extent.width * scale)

// Apply resizing
resizeFilter.setValue(ciImage, forKey: kCIInputImageKey)
resizeFilter.setValue(scale, forKey: kCIInputScaleKey)
resizeFilter.setValue(aspectRatio, forKey: kCIInputAspectRatioKey)
let outputImage = resizeFilter.outputImage

let croppedImage = outputImage!.cropped(to: CGRect(x: viewX, y: viewY, width: widht, height: height))

Now with the croppedImage (the card image) I can use Vision to make the OCR.

let request = VNRecognizeTextRequest()
request.recognitionLevel = .accurate
request.usesLanguageCorrection = false

let stillImageRequestHandler = VNImageRequestHandler(ciImage: croppedImage, options: [:])
try? stillImageRequestHandler.perform([request])

guard let texts = request.results as? [VNRecognizedTextObservation], texts.count > 0 else {
    // no text detected
    return
}

let arrayLines = texts.flatMap({ $0.topCandidates(20).map({ $0.string }) })

The arrayLines contains the 20 most accurate lines recognised.

Now with arrayLines I need to check if is card number, date or CVV.

if creditCardDate == nil &&
    trimmed.count >= 5 && // 12/20
    trimmed.count <= 7 && // 12/2020
    trimmed.isDate {
    
    creditCardDate = line
    DispatchQueue.main.async {
        self.labelCardDate?.text = line
        self.tapticFeedback()
    }
    continue
}

It’s not hard, but the secret are the extensions:

// Date Pattern MM/YY or MM/YYYY
var isDate: Bool {
    let arrayDate = components(separatedBy: "/")
    if arrayDate.count == 2 {
        let currentYear = Calendar.current.component(.year, from: Date())
        if let month = Int(arrayDate[0]), let year = Int(arrayDate[1]) {
            if month > 12 || month < 1 {
                return false
            }
            if year < (currentYear - 2000 + 20) && year >= (currentYear - 2000) { // Between current year and 20 years ahead
                return true
            }
            if year >= currentYear && year < (currentYear + 20) { // Between current year and 20 years ahead
                return true
            }
        }
    }
    return false
}

That’s it!

I hope can I help you! If you like, please give your star on the Github Project.

1+