Native QR Code Generation in Swift

Let's see how we can use native Swift code to generate a variety of different barcode and QR code types.

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!


Subscribe to Digital Bunker

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
[email protected]
Subscribe