📷

SwiftUIでQRコード作成&読み取りを行う

2025/02/08に公開

概要

タイトルの通りですが、SwiftUIでQRコードの生成と読み取りを試して見たいと思います。

iOS 13.0以降から👇の便利なメソッドを使えるので、こちらを使って生成していきたいと思います。

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

このメソッドは、QRコードを画像として生成します。 QRコードは、ISO/IEC 18004:2006規格で定義されている高密度マトリックスバーコードフォーマットです。
QRコード・ジェネレーター・フィルターは、以下のプロパティを使用します
・message: NSData として QR コードとしてエンコードされるデータを表す文字列
・correctionLevel: NSStringとしてエラー訂正形式を表す1文字の文字列。 Lは7パーセント補正、Mは15パーセント補正、Qは25パーセント補正、Hは30パーセント補正。

動作環境

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

シンプルな実装

まずは与えられた文字列をQRコードとして表示するだけのシンプルなViewを作成していきたいと思います。

QrCodeView.swift を作成し、以下実装を追加します。

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)
}

① :CoreImage.CIFilterBuiltins のimport

② : 画像拡大縮小時の補間をなくす

※ 補間は、画像を拡大または縮小するときに、ピクセル間を埋めたりスムーズに表示するためのアルゴリズム。QRコードでは読み取りエラーを起こす可能性がある。

③ : アスペクト比を保ちながらリサイズ可能にする

④ : CIFilter.qrCodeGenerator を使用して出力画像を UIImage に変換している

👇Preview表示の様子

image1.png

JSON文字列をQRコード化する

次にJSON文字列を受け取ってQRコードを生成させてみたいと思います。新規にJsonQrCodeView.swift を作成します。

まずはJSON化するオブジェクトを定義します。仮に Book というオブジェクトを定義します。

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

この Book を受け取ってJSON文字列化してQRコード生成します。

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)
}

👇Preview表示の様子

image2.png

こちらをiOSのカメラでフォーカスするとJSON文字列が表示されるかと思います。

QRコード読み取り

次にQRコードの読み取りを実装していきたいと思います。

実装の手段としてはざっと調べて以下の3つの方法があるみたいです。

  1. サードパーティのライブラリを使う
    • 有名どころだと CodeScanner がありそうです
  2. AVFoundation を使用して実装する
  3. VisionKitDataScannerViewControllerを使用して実装する
    • iOS 16以降のみ使用可能

今回はVisionKitDataScannerViewControllerを試してみたいと思います。

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

準備

カメラを使うので Info.plistPrivacy - Camera Usage Description の設定を追加します。

Value は何の為に使うか理由を書いとけばOKです。

※ Xcode13以降の場合はデフォルトで Info.plist は作られないので、「TARGETS」>「Info」タブ >「Custom macOS Application Target Properties」から追加して下さい。

またカメラを使う際に実機でないと使えない為、実機で動作させる為の設定も必要になります。

👇参考URL

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

実装

新規 JsonQrCodeReaderView.swift を以下内容で作成します。

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()
}

① : VisionKit をimportしています

② : DataScannerViewController を作成しています

  • recognizedDataTypes
    • recognizedDataTypes: Set<DataScannerViewController.RecognizedDataType>
    • text(languages:textContentType:) 又は barcode(symbologies:) を指定します
    • 今回は ORコードなので [.barcode(symbologies: [.qr])] を指定しています
  • isHighlightingEnabled
    • ORコードを認識したらハイライトされるように true に設定しています

③ : カメラ起動しScanを開始するようにしています

👇実機で動作させている様子

image3.gif

読み取った内容を表示

最後に、読み取ったQRコードの内容を画面上に表示させてみたいと思います。

👇 完成形の実装になります。

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()
        }
    }
}

① : Delegateから使えるように DataScannerViewController を外出ししています

② : DataScannerViewControllerDelegate でイベントのやりとりができる様に makeCoordinatorCoordinator クラスを返しています

③ : QRコードが認識されたら呼ばれます。barcode: RecognizedItem.Barcodebounds に認識した領域がセットされているので、その領域に読み取った文字列を UITextView として作成し、dataScannerViewControllerの overlayContainerView にViewを追加しています

④ : QRコードが認識が停止されたら呼ばれます。今回は1つだけしか認識させない前提なので、③で作成した UITextView を削除してます

👇実機で動作させている様子

image4.gif

参考URL

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