Native QR Code Generation in Swift

Several months ago, I was working on a developer tool called DeepLinkr. The tool was designed to help developers document and test their applications' deep and universal links.

As part of the feature set, I wanted DeepLinkr to generate a QR code that the developer could scan to simulate scanning a QR code in the wild. Initially, I was searching for 3rd-party libraries that could support this functionality, but it turns out that you can create a wide variety of QR and Barcode types using native Swift.

CIFilter provides support for the following four barcode and QR code variants:

Aztec Code - Wikipedia
Code 128 - Wikipedia
PDF417 - Wikipedia
QR code - Wikipedia

As a starting point, we can use the sample implementation from Hacking With Swift:

func generateBarcode(from string: String) -> UIImage? {
    let data = string.data(using: String.Encoding.ascii)

    if let filter = CIFilter(name: "CICode128BarcodeGenerator") {
        filter.setValue(data, forKey: "inputMessage")
        let transform = CGAffineTransform(scaleX: 3, y: 3)

        if let output = filter.outputImage?.transformed(by: transform) {
            return UIImage(ciImage: output)
        }
    }

    return nil
}

Let's create a wrapper around this implementation to make it more convenient to use.

All of these barcode variations require us to specify a name for the specific CIFilter we want to use and a set of key-value pairs to specify the barcode's styling and behavior.

Let's formalize this requirement with a protocol.

protocol Barcodable {
    var name: String { get }
    var properties: [String: Any] { get }
}

This allows us to clean up the implementation and transform it into this instead:

struct BarcodeService {
    static func generateBarcode(from barcode: Barcodable, scalar: CGFloat = 3.0) -> UIImage? {
        if let filter = CIFilter(name: barcode.name) {
            filter.setValuesForKeys(barcode.properties)

            let transform = CGAffineTransform(scaleX: scalar, y: scalar)
            if let output = filter.outputImage?.transformed(by: transform) {
                return UIImage(ciImage: output)
            }
        }

        return nil
    }
}

Now, we just need to ensure the barcode type we need conforms to this protocol.

Below you'll find a sample implementation - feel free to use whichever one you need.

Aztec Barcode

/// Generate an Aztec barcode image for message data.
struct AztecBarcode: Barcodable {
    let name = "CIAztecCodeGenerator"

    /// Force a compact style Aztec code to true or false. Set to nil for automatic.
    let inputCompactStyle: Bool?

    /// Aztec error correction value between 5 and 95
    let inputCorrectionLevel: NSNumber

    /// Aztec layers value between 1 and 32. Set to nil for automatic.
    let inputLayers: NSNumber?

    /// The message to encode in the Aztec Barcode
    let inputMessage: Data

    init(inputCompactStyle: Bool? = nil, inputCorrectionLevel: NSNumber = 5.0, inputLayers: NSNumber? = nil, inputMessage: Data) throws {
        self.inputCompactStyle = inputCompactStyle
        self.inputCorrectionLevel = inputCorrectionLevel
        self.inputLayers = inputLayers
        self.inputMessage = inputMessage
    }

    var properties: [String: Any] {
        var response: [String: Any] = [:]

        if let inputCompactStyle = inputCompactStyle {
            response["inputCompactStyle"] = inputCompactStyle
        }

        response["inputCorrectionLevel"] = inputCorrectionLevel

        if let inputLayers = inputLayers {
            response["inputLayers"] = inputLayers
        }

        response["inputMessage"] = NSData(data: inputMessage)

        return response
    }
}

// Usage:
if let data = "http://www.digitalbunker.dev".data(using: .ascii), let aztecBarcode = try? AztecBarcode(inputMessage: data) {
    imageView.image = BarcodeService.generateBarcode(from: aztecBarcode)
}

QRCode

/// Generate a QR Code image for message data.
struct QRCode: Barcodable {
    enum QRCorrectionLevel: String {
        case l
        case m
        case q
        case h
    }

    let name = "CIQRCodeGenerator"

    /// QR Code correction level L, M, Q, or H.
    let inputCorrectionLevel: QRCorrectionLevel = .m

    /// The message to encode in the QR Code
    let inputMessage: Data

    var properties: [String: Any] {
        [
            "inputCorrectionLevel": inputCorrectionLevel.rawValue.uppercased(),
            "inputMessage": NSData(data: inputMessage)
        ]
    }
}

// Usage:
if let data = "http://www.digitalbunker.dev".data(using: .ascii) {
    let qrCode = QRCode(inputMessage: data)
    imageView.image = BarcodeService.generateBarcode(from: qrCode)
}

PDF417

/// Generate a PDF417 barcode image for message data.
struct PDF417Barcode: Barcodable {
    let name = "CIPDF417BarcodeGenerator"

    /// The message to encode in the PDF417 Barcode
    let inputMessage: Data

    /// The minimum width of the generated barcode in pixels. (Number. Min: 56.0 Max: 583.0)
    let inputMinWidth: NSNumber

    /// The maximum width of the generated barcode in pixels. (Number. Min: 56.0 Max: 583.0)
    let inputMaxWidth: NSNumber

    /// The minimum height of the generated barcode in pixels. (Number. Min: 13.0 Max: 283.0)
    let inputMinHeight: NSNumber

    /// The maximum height of the generated barcode in pixels. (Number. Min: 13.0 Max: 283.0)
    let inputMaxHeight: NSNumber

    /// The number of data columns in the generated barcode (Number. Min: 1.0 Max: 30.0)
    let inputDataColumns: NSNumber

    /// The number of rows in the generated barcode (Number. Min: 3.0 Max: 90.0)
    let inputRows: NSNumber

    /// The preferred aspect ratio of the generated barcode (Number. Min: 0.0)
    let inputPreferredAspectRatio: NSNumber

    /// The compaction mode of the generated barcode. (Number. Min: 0.0 Max: 3.0)
    let inputCompactionMode: NSNumber

    /// Force a compact style Aztec code to @YES or @NO. Set to nil for automatic. (Number. Min: 0.0 Max: 1.0)
    let inputCompactStyle: Bool

    /// The correction level ratio of the generated barcode (Number. Min: 0.0 Max: 8.0)
    let inputCorrectionLevel: NSNumber

    /// Force compaction style to @YES or @NO. Set to nil for automatic. (Number. Min: 0.0 Max: 1.0)
    let inputAlwaysSpecifyCompaction: Bool

    var properties: [String: Any] {
        ["inputMessage": inputMessage as NSData,
         "inputMinWidth": inputMinWidth,
         "inputMaxWidth": inputMaxWidth,
         "inputMinHeight": inputMinHeight,
         "inputMaxHeight": inputMaxHeight,
         "inputDataColumns": inputDataColumns,
         "inputRows": inputRows,
         "inputPreferredAspectRatio": inputPreferredAspectRatio,
         "inputCompactionMode": inputCompactionMode,
         "inputCompactStyle": inputCompactStyle as NSNumber,
         "inputCorrectionLevel": inputCorrectionLevel,
         "inputAlwaysSpecifyCompaction": inputAlwaysSpecifyCompaction as NSNumber
        ]
    }
}

// Usage:
if let data = "http://www.digitalbunker.dev".data(using: .ascii) {
    let pdfBarcode = PDF417Barcode(inputMessage: data, inputMinWidth: 100, inputMaxWidth: 100, inputMinHeight: 100, inputMaxHeight: 100, inputDataColumns: 10, inputRows: 10, inputPreferredAspectRatio: 3, inputCompactionMode: 2, inputCompactStyle: true, inputCorrectionLevel: 2, inputAlwaysSpecifyCompaction: true)
    imageView.image = BarcodeService.generateBarcode(from: pdfBarcode)
}

Code128

/// Generate a Code 128 barcode image for message data.
struct Code128Barcode: Barcodable {
    let name = "CICode128BarcodeGenerator"

    /// The message to encode in the Code 128 Barcode
    let inputMessage: Data

    /// The number of empty white pixels that should surround the barcode. (Scalar. Min: 0.0 Max: 100.0)
    let inputQuietSpace: NSNumber

    /// The height of the generated barcode in pixels. (Scalar. Min: 1.0 Max: 500.0)
    let inputBarcodeHeight: NSNumber

    var properties: [String: Any] {
        [
            "inputBarcodeHeight": inputBarcodeHeight,
            "inputQuietSpace": inputQuietSpace,
            "inputMessage": inputMessage as NSData
        ]
    }
}

// Usage:
if let data = "http://www.digitalbunker.dev".data(using: .ascii) {
    let code128Barcode = Code128Barcode(inputMessage: data, inputQuietSpace: 20, inputBarcodeHeight: 100)
    imageView.image = BarcodeService.generateBarcode(from: code128Barcode)
}

If you're interested in more articles about iOS Development & Swift, check out my YouTube channel or follow me on Twitter.

If you want to be notified whenever I post a new article, join the mailing list below.


Do you have an iOS Interview coming up?

Check out my book Ace The iOS Interview!