SwiftUIでQRコードを読み取る。

# はじめに
SwiftUIでQRコードを読み取るアプリ

SwiftUIでQRコード読み取り箇所は下記を参考にしました。
https://blog.devgenius.io/camera-preview-and-a-qr-code-scanner-in-swiftui-48b111155c66
本記事では、シュミレータでの確認処理など取り除いてあるので、
必要な方はリンク先を参照して下さい。

# 環境
macOS Big Sur 11.3
XCode 12.5
Swift 5

# アプリの概要
(1).カメラ起動をタップして、QRコード読み取り画面を表示する。
(2).QRコード読み取りが成功したら、最初の画面に戻る。
(3).QRコードで読み取った文字列を表示する。

![名称未設定のノート (4)-2 2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16101/e4f1a7a6-11ec-4c96-44af-35f50722b63e.jpeg)

# QRコード読み取り関連のクラス
Viewの状態を保持するModel

```swift:ScannerViewModel.swift
import Foundation

class ScannerViewModel: ObservableObject {
    
    /// QRコードを読み取る時間間隔
    let scanInterval: Double = 1.0
    @Published var lastQrCode: String = "QRコード"
    @Published var isShowing: Bool = false

    /// QRコード読み取り時に実行される。
    func onFoundQrCode(_ code: String) {
        self.lastQrCode = code
        isShowing = false
    }
}
```

QRコードが見つかったかどうかをチェックして、そのQRコードの値を親Viewに通知するデリゲート

```swift:QrCodeCameraDelegate.swift
import AVFoundation

class QrCodeCameraDelegate: NSObject, AVCaptureMetadataOutputObjectsDelegate {
    
    var scanInterval: Double = 1.0
    var lastTime = Date(timeIntervalSince1970: 0)
    
    var onResult: (String) -> Void = { _  in }
    var mockData: String?
    
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        if let metadataObject = metadataObjects.first {
            guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
            guard let stringValue = readableObject.stringValue else { return }
            foundBarcode(stringValue)
        }
    }
    
    @objc func onSimulateScanning(){
        foundBarcode(mockData ?? "Simulated QR-code result.")
    }
    
    func foundBarcode(_ stringValue: String) {
        let now = Date()
        if now.timeIntervalSince(lastTime) >= scanInterval {
            lastTime = now
            self.onResult(stringValue)
        }
    }
}
```

カメラの映像を表示するためのUIViewです。
これはUIKitのビューで、最終的にはSwiftUIのレイアウトでUIViewRepresentableを使って表示されます。

```swift:CameraPreview.swift
import UIKit
import AVFoundation

class CameraPreview: UIView {
    
    private var label:UILabel?
    
    var previewLayer: AVCaptureVideoPreviewLayer?
    var session = AVCaptureSession()
    weak var delegate: QrCodeCameraDelegate?
    
    init(session: AVCaptureSession) {
        super.init(frame: .zero)
        self.session = session
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
        
    @objc func onClick(){
        delegate?.onSimulateScanning()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        previewLayer?.frame = self.bounds
    }
}
```
QRコード読み取りView

```swift:QrCodeScannerView.swift
import SwiftUI
import AVFoundation

struct QrCodeScannerView: UIViewRepresentable {
    
    var supportedBarcodeTypes: [AVMetadataObject.ObjectType] = [.qr]
    typealias UIViewType = CameraPreview
    
    private let session = AVCaptureSession()
    private let delegate = QrCodeCameraDelegate()
    private let metadataOutput = AVCaptureMetadataOutput()
        
    func interval(delay: Double) -> QrCodeScannerView {
        delegate.scanInterval = delay
        return self
    }
    
    func found(r: @escaping (String) -> Void) -> QrCodeScannerView {
        print("found")
        delegate.onResult = r
        return self
    }
        
    func setupCamera(_ uiView: CameraPreview) {
        if let backCamera = AVCaptureDevice.default(for: AVMediaType.video) {
            if let input = try? AVCaptureDeviceInput(device: backCamera) {
                session.sessionPreset = .photo
                
                if session.canAddInput(input) {
                    session.addInput(input)
                }
                if session.canAddOutput(metadataOutput) {
                    session.addOutput(metadataOutput)
                    
                    metadataOutput.metadataObjectTypes = supportedBarcodeTypes
                    metadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main)
                }
                let previewLayer = AVCaptureVideoPreviewLayer(session: session)
                
                uiView.backgroundColor = UIColor.gray
                previewLayer.videoGravity = .resizeAspectFill
                uiView.layer.addSublayer(previewLayer)
                uiView.previewLayer = previewLayer
                
                session.startRunning()
            }
        }
        
    }
    
    func makeUIView(context: UIViewRepresentableContext<QrCodeScannerView>) -> QrCodeScannerView.UIViewType {
        let cameraView = CameraPreview(session: session)
        
        checkCameraAuthorizationStatus(cameraView)
        
        return cameraView
    }
    
    static func dismantleUIView(_ uiView: CameraPreview, coordinator: ()) {
        uiView.session.stopRunning()
    }
    
    private func checkCameraAuthorizationStatus(_ uiView: CameraPreview) {
        let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
        if cameraAuthorizationStatus == .authorized {
            setupCamera(uiView)
        } else {
            AVCaptureDevice.requestAccess(for: .video) { granted in
                DispatchQueue.main.sync {
                    if granted {
                        self.setupCamera(uiView)
                    }
                }
            }
        }
    }
    
    func updateUIView(_ uiView: CameraPreview, context: UIViewRepresentableContext<QrCodeScannerView>) {
        uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
        uiView.setContentHuggingPriority(.defaultLow, for: .horizontal)
    }
    
}
```
# 画面関連のクラス
最初に表示する画面

```swift:ContentView.swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = ScannerViewModel()
    
    var body: some View {

        VStack {
            Text("QR Code Reader")
                .padding()
            
            // 読み取ったQRコード表示位置
            Text("URL = [ " + viewModel.lastQrCode + " ]")
            
            Button(action: {
                viewModel.isShowing = true
            }){
                Text("カメラ起動")
                Image(systemName: "camera")
            }
            .fullScreenCover(isPresented: $viewModel.isShowing) {
                SecondView(viewModel: viewModel)
            }
        }
    }
}
```
2画面目
QRコード読み取りが成功したら最初の画面に戻ります。

```swift:SecondView.swift
import SwiftUI

struct SecondView: View {
    @ObservedObject var viewModel : ScannerViewModel

    var body: some View {
        Text("SecondView")
        
        ZStack {
            // QRコード読み取りView
            QrCodeScannerView()
                .found(r: self.viewModel.onFoundQrCode)
                .interval(delay: self.viewModel.scanInterval)
            
            VStack {
                VStack {
                    Text("Keep scanning for QR-codes")
                        .font(.subheadline)
                    
                    Text("QRコード読み取り結果 = [ " + self.viewModel.lastQrCode + " ]")
                        .bold()
                        .lineLimit(5)
                        .padding()
                    
                    Button("Close") {
                        self.viewModel.isShowing = false
                    }
                }
                .padding(.vertical, 20)
                Spacer()
            }.padding()
        }
    }
}
```

# info.plist
カメラ使用許可設定を忘れずに
これを設定しないと、カメラ起動時にアプリが落ちます。

	<key>NSCameraUsageDescription</key>
	<string>QRCodeを読み取る為に使用します。</string>

# 最後に
アドバイス、ご指摘などコメント頂けるとありがたいです。