【iOS/SwiftUI】QRコードリーダーの生成

目次

概要

最近はQRコードを読み取る場面が増えている。
そのため、今後何かサービスを開始する際もQRコードリーダーがあった方がいいだろう。人が求めるものは何かの読み取りや画像認識が多い印象もあるし。
そのため、最低限のQRコードの読み取り処理を実装する。

ソースコード

TestApp.swift

import SwiftUI

@main
struct TestApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    
    var body: some Scene {
        WindowGroup {
            QrScanView()
        }
    }
}

QrScanView.swift

import SwiftUI

struct QrScanView: View {
    @ObservedObject private(set) var viewModel = QrScanViewModel()
    
    var body: some View {
        ZStack {
            QrCapturePreview(qrScanViewModel: viewModel)
            VStack {
                Spacer()
            }
            DialogView()
        }
    }
    
    private func DialogView() -> some View {
        return ZStack {
            Text("")
                .alert(isPresented: $viewModel.isShownDialog) {
                    Alert(title: Text("読み取ったQRコード"),
                          message: Text(viewModel.qrString),
                          dismissButton: .default(Text("閉じる")) {}
                    )
                }
        }
    }
}

struct QrCapturePreview: UIViewRepresentable {
    private var qrScanViewModel: QrScanViewModel
    
    init(qrScanViewModel: QrScanViewModel) {
        self.qrScanViewModel = qrScanViewModel
    }
    
    public func makeUIView(context: Context) -> some UIView {
        
        let cameraView = qrScanViewModel.previewView
        cameraView.frame = UIScreen.main.bounds
        
        self.qrScanViewModel.launchQrcodeReader()
        
        return cameraView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
}

QrScanViewModel

import AVFoundation
import Foundation
import UIKit

class QrScanViewModel: NSObject, ObservableObject {
    @Published var isShownDialog = false
    @Published private(set) var qrString = ""
    
    var previewView: UIView = UIView()
    private let metadataOutput = AVCaptureMetadataOutput()
    private var session: AVCaptureSession?
    
    func launchQrcodeReader() {
        if let session = self.session {
            DispatchQueue.global(qos: .background).async {
                session.startRunning()
            }
            return
        }
        
        let session = AVCaptureSession()
        guard let device = AVCaptureDevice.default(.builtInDualWideCamera, for: .video, position: .back),
              let deviceInput = try? AVCaptureDeviceInput(device: device) else {
            return
        }
        
        switch UIDevice.current.userInterfaceIdiom {
        case .phone:
            session.sessionPreset = .high
        default:
            session.sessionPreset = .photo
        }
        
        if session.canAddInput(deviceInput) {
            session.addInput(deviceInput)
        }
        if session.canAddOutput(metadataOutput) {
            session.addOutput(metadataOutput)
        }
        
        metadataOutput.metadataObjectTypes = [.qr]
        metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
        
        DispatchQueue.main.async {
            let videoPreviewLayer = AVCaptureVideoPreviewLayer(session: session)
            videoPreviewLayer.videoGravity = .resizeAspectFill
            videoPreviewLayer.connection?.videoOrientation = .portrait
            videoPreviewLayer.frame = self.previewView.bounds
            self.previewView.layer.insertSublayer(videoPreviewLayer, at: 0)
        }
        
        DispatchQueue.global(qos: .background).async {
            session.startRunning()
        }
        
        self.session = session
    }
}

extension QrScanViewModel: AVCaptureMetadataOutputObjectsDelegate {
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        for metadataObject in metadataObjects {
            guard let machineReadableCode = metadataObject as? AVMetadataMachineReadableCodeObject,
                  machineReadableCode.type == .qr,
            let stringValue = machineReadableCode.stringValue else {
                return
            }
            self.qrString = stringValue
            self.isShownDialog = true
        }
    }
}

デモ動画

詳細

まずカメラの映像を表示する

基本的にはカメラの映像を取得する点は同じ。(ただし、カメラの撮影ボタンを押下した時の処理は削除)

QRコード読み取り処理

QRコードの読み取り処理は以下の部分。基本的に、読み取ったものがQRコードだった場合にその情報の文字列を取得する。

extension QrScanViewModel: AVCaptureMetadataOutputObjectsDelegate {
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        for metadataObject in metadataObjects {
            guard let machineReadableCode = metadataObject as? AVMetadataMachineReadableCodeObject,
                  machineReadableCode.type == .qr,
            let stringValue = machineReadableCode.stringValue else {
                return
            }
            self.qrString = stringValue
            self.isShownDialog = true
        }
    }
}

上記の処理が動くようにするために以下の実装が必要。
このsetMetadataObjectsDelegateの実装をすることでQRコードを読み取ったら上記の処理が走るようになる。
一応、メインスレッドで行うようにしているが、今のところ特に理由はない。

if session.canAddOutput(metadataOutput) {
    session.addOutput(metadataOutput)
}

metadataOutput.metadataObjectTypes = [.qr]
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)

参考ページ