iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
📷

Generating and Scanning QR Codes with SwiftUI

に公開

Overview

As the title suggests, I would like to try generating and reading QR codes using SwiftUI.

Since iOS 13.0 and later, the following convenient method is available, so I will use it for generation.

https://developer.apple.com/documentation/coreimage/cifilter/3228262-qrcodegenerator

This method generates a QR code as an image. A QR code is a high-density matrix barcode format defined in the ISO/IEC 18004:2006 standard.
The QR code generator filter uses the following properties:
・message: A string representing the data to be encoded as a QR code as NSData
・correctionLevel: A single-character string representing the error correction format as NSString. L is 7% correction, M is 15% correction, Q is 25% correction, H is 30% correction.

Environment

  • MBA M3 24GB Sonoma 14.6.1
  • Xcode: Version 16.1 (16B40)

Simple Implementation

First, I would like to create a simple View that just displays a given string as a QR code.

Create QrCodeView.swift and add the following implementation.

import CoreImage.CIFilterBuiltins // ①
import SwiftUI

struct QrCodeView: View {
    var data: String
    var body: some View {
        Image(uiImage: qrImage)
            .interpolation(.none) // ②
            .resizable() // ③
            .scaledToFit()
            .accessibilityLabel(Text("QRCode"))
    }

    private var qrImage: UIImage { // ④
        let qrCodeGenerator = CIFilter.qrCodeGenerator()
        qrCodeGenerator.message = Data(data.utf8)
        qrCodeGenerator.correctionLevel = "H"
        if let outputimage = qrCodeGenerator.outputImage {
            if let cgImage = CIContext().createCGImage(
                outputimage, from: outputimage.extent) {
                return UIImage(cgImage: cgImage)
            }
        }
        return UIImage()
    }
}

#Preview {
    QrCodeView(data: "abc")
        .frame(width: 150, height: 150)
}

① : Importing CoreImage.CIFilterBuiltins

② : Removing interpolation when scaling the image

  • Interpolation is an algorithm used to fill between pixels or display them smoothly when enlarging or shrinking an image. For QR codes, it may cause scanning errors.

③ : Making the image resizable while maintaining the aspect ratio

④ : Converting the output image to UIImage using CIFilter.qrCodeGenerator

👇 How it looks in the Preview

image1.png

Converting JSON Strings to QR Codes

Next, I would like to try generating a QR code by receiving a JSON string. I will create a new file named JsonQrCodeView.swift.

First, let's define the object to be converted to JSON. Let's assume we define a Book object.

struct Book: Codable {
    let id: Int
    let title: String
    let author: String
    let publicationAt: Date
}

We will receive this Book, convert it into a JSON string, and generate a QR code.

struct JsonQrCodeView: View {
    var book: Book
    var body: some View {
        if let json = jsonStr {
            QrCodeView(data: json)
                .frame(width: 150, height: 150)
        } else {
            Text("Qr Code Generate Error")
        }
    }

    private var jsonStr: String? {
        do {
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.prettyPrinted, .withoutEscapingSlashes]
            let jsonData = try encoder.encode(book)
            return String(data: jsonData, encoding: .utf8)
        } catch {
            print(error.localizedDescription)
        }
        return nil
    }
}

#Preview {
    let book = Book(
        id: 1,
        title: "こころ",
        author: "夏目漱石",
        publicationAt: Date()
    )
    JsonQrCodeView(book: book)
}

👇 How it looks in the Preview

image2.png

If you focus on this with an iOS camera, the JSON string should be displayed.

QR Code Scanning

Next, I would like to implement QR code scanning.

From a quick search, there seem to be about three ways to implement this:

  1. Use a third-party library
  2. Implement using AVFoundation
  3. Implement using VisionKit's DataScannerViewController
    • Only available on iOS 16 and later

In this article, I will try using VisionKit's DataScannerViewController.

https://developer.apple.com/documentation/visionkit/datascannerviewcontroller

Preparation

Since the camera will be used, add the Privacy - Camera Usage Description setting to Info.plist.

For the Value, just write the reason for using the camera.

  • Note: For Xcode 13 and later, Info.plist is not created by default. Please add it from "TARGETS" > "Info" tab > "Custom macOS Application Target Properties".

Also, since the camera can only be used on a physical device, you will need to configure settings to run it on an actual device.

👇 Reference URL

https://www.ingenious.jp/articles/howto/xcode/xcode-actual-machine-test/

Implementation

Create a new file JsonQrCodeReaderView.swift with the following content.

import SwiftUI
import VisionKit // ①

struct QRCodeScanner: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> DataScannerViewController {
        // ②
        let dataScannerViewController = DataScannerViewController(
            recognizedDataTypes: [.barcode(symbologies: [.qr])],
            isHighlightingEnabled: true
        )
        try? dataScannerViewController.startScanning() // ③
        return dataScannerViewController
    }

    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {
    }
}

struct JsonQrCodeReaderView: View {
    var body: some View {
        QRCodeScanner()
    }
}

#Preview {
    JsonQrCodeReaderView()
}

① : Importing VisionKit

② : Creating DataScannerViewController

  • recognizedDataTypes
    • recognizedDataTypes: Set<DataScannerViewController.RecognizedDataType>
    • Specify text(languages:textContentType:) or barcode(symbologies:)
    • Since we are dealing with QR codes, [.barcode(symbologies: [.qr])] is specified
  • isHighlightingEnabled
    • Set to true to highlight when a QR code is recognized

③ : Starting the camera and beginning the scan

👇 How it looks running on an actual device

image3.gif

Displaying the Scanned Content

Finally, I would like to display the content of the scanned QR code on the screen.

👇 Here is the final implementation.

struct QRCodeScanner: UIViewControllerRepresentable {
    // ①
    var dataScannerViewController = DataScannerViewController(
        recognizedDataTypes: [.barcode(symbologies: [.qr])],
        isHighlightingEnabled: true
    )

    func makeUIViewController(context: Context) -> DataScannerViewController {
        dataScannerViewController.delegate = context.coordinator
        try? dataScannerViewController.startScanning()
        return dataScannerViewController
    }

    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {
    }
    // ②
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    final class Coordinator: NSObject, DataScannerViewControllerDelegate {
        private let parent: QRCodeScanner
        private var textView: UIView?

        init(_ parent: QRCodeScanner) {
            self.parent = parent
        }

        func dataScanner(_ dataScanner: DataScannerViewController,
                         didAdd addedItems: [RecognizedItem],
                         allItems: [RecognizedItem]
        ) {
            guard case .barcode(let barcode) = addedItems.first else {
                return
            }
            // ③
            if let text = barcode.payloadStringValue {
                let frame = CGRect(
                    x: barcode.bounds.topLeft.x,
                    y: barcode.bounds.topLeft.y,
                    width: abs(barcode.bounds.topRight.x - barcode.bounds.topLeft.x) + 15,
                    height: abs(barcode.bounds.topLeft.y - barcode.bounds.bottomLeft.y) + 15
                )
                let textView = UITextView(frame: frame)
                textView.font = UIFont.systemFont(ofSize: 10)
                textView.text = text
                parent.dataScannerViewController.overlayContainerView.addSubview(textView)
                self.textView = textView
            }
        }
        // ④
        func dataScanner(_ dataScanner: DataScannerViewController,
                         didRemove removedItems: [RecognizedItem],
                         allItems: [RecognizedItem]
        ) {
            self.textView?.removeFromSuperview()
        }
    }
}

① : DataScannerViewController is extracted so that it can be used from the Delegate.

② : The Coordinator class is returned in makeCoordinator to enable event interaction via DataScannerViewControllerDelegate.

③ : Called when a QR code is recognized. Since the recognized area is set in the bounds of barcode: RecognizedItem.Barcode, a UITextView is created with the scanned string in that area and added as a subview to the dataScannerViewController's overlayContainerView.

④ : Called when the QR code recognition stops. Since it is assumed that only one item will be recognized at a time, the UITextView created in ③ is removed.

👇 How it looks running on an actual device

image4.gif

Reference URLs

https://qiita.com/ikaasamay/items/58d1a401e98673a96fd2

https://medium.com/@ramesh_aran86/how-to-use-visionkit-in-swiftui-for-text-and-barcode-scanning-on-ios-e3f66c9006f2

Discussion