SwiftUIでQRスキャンする

## 概要
SwftUIでQRコードをスキャンするときのメモです。


## 環境
`Xcode 14.2`

## 実装手順
1. カメラを起動するUIViewを実装する
2. 実装したUIViewをSiwftUIで扱えるようにする
3. カメラから受け取ったアウトプットをViewModelでハンドリングする

### カメラを起動する`UIView`を実装
セッションに関する設定をします。

```swift:UICameraView.swift 
import THLogger
import UIKit
import AVFoundation

class UICameraView: UIView {
    private let metadataOutput = AVCaptureMetadataOutput()
    private var previewLayer: AVCaptureVideoPreviewLayer?
    private var captureSession: AVCaptureSession = .init()
    private var videoDevice: AVCaptureDevice? = .default(for: .video)
    
    weak var delegate: UICameraViewDelegate?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        do {
            try configurePreviewLayer()
        } catch {
            THLogger.error(error)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        previewLayer?.frame = bounds
    }
}

// MARK: - CONFIGURE
extension UICameraView {
    private func configurePreviewLayer() throws {
        guard let videoDevice = videoDevice else { return }
        
        let input = try AVCaptureDeviceInput(device: videoDevice)
        captureSession.addInput(input)
        
        if captureSession.canAddOutput(metadataOutput) {
            captureSession.addOutput(metadataOutput)
            metadataOutput.metadataObjectTypes = [.qr]
            metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
        }
        
        captureSession.beginConfiguration()
        if captureSession.canSetSessionPreset(.photo) {
            captureSession.sessionPreset = .photo
        }
        captureSession.commitConfiguration()
        
        DispatchQueue.global().async {
            self.captureSession.startRunning()
        }
        
        previewLayer = .init(session: captureSession)
        previewLayer?.videoGravity = .resizeAspectFill
        layer.addSublayer(previewLayer!)
    }
}

// MARK: - AVCaptureMetadataOutputObjectsDelegate
extension UICameraView: AVCaptureMetadataOutputObjectsDelegate {
    func metadataOutput(
        _ output: AVCaptureMetadataOutput,
        didOutput metadataObjects: [AVMetadataObject],
        from connection: AVCaptureConnection
    ) {
        delegate?.didOutput(output, metadataObjects: metadataObjects)
    }
}

```

### `UIViewRepresentable`
`UICameraView`を`UIViewRepresentable`と`UICameraViewDelegate`に準拠させます。
その後、`PassthroughSubject`を使ってdelegateのデータを親クラスに渡しています。

```swift:CameraViewRepresentable.swift
import Combine
import AVFoundation
import SwiftUI

struct DidOutputObjects {
    let output: AVCaptureMetadataOutput
    let metadataObjects: [AVMetadataObject]
}

struct CameraViewRepresentable: UIViewRepresentable {
    private let didOutputSubject: PassthroughSubject<DidOutputObjects, Never>
    
    init(didOutputSubject: PassthroughSubject<DidOutputObjects, Never>) {
        self.didOutputSubject = didOutputSubject
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeUIView(context: Context) -> UICameraView {
        let view = UICameraView()
        view.delegate = context.coordinator
        return view
    }
    
    func updateUIView(_ uiView: UICameraView, context: Context) {
    }
    
    class Coordinator: NSObject, UICameraViewDelegate {
        
        let parent: CameraViewRepresentable
        
        init(_ parent: CameraViewRepresentable) {
            self.parent = parent
        }
        
        func didOutput(_ output: AVCaptureMetadataOutput, metadataObjects: [AVMetadataObject]) {
            parent.didOutputSubject.send(.init(output: output, metadataObjects: metadataObjects))
        }
    }
}

```

### `ViewModel`

`PassthroughSubject`でQRコードのデータが渡ってくるので、以下のようにして受け取り画面に表示します。

```swift:ScannerViewModel.swift
extension ScannerViewModel {
    private func subscribeDidOutput() {
        didOutputSubject
            .receive(on: DispatchQueue.main)
            .sink { [weak self] objects in
                guard let self = self else { return }
                guard let metadataObject = objects.metadataObjects.first else { return }
                guard let readable = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                guard let stringValue = readable.stringValue else { return }
                self.lastQrCode = stringValue
            }
            .store(in: &cancellables)
    }
}
```

全体のソースコードは[こちら](https://github.com/h-taro/qr-scan-demo)に載せております。

QRコードを読み取り終わり、画面は以下のようになりました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/c9df25bf-20a6-8dbc-dcc4-6ffb34967eb7.jpeg" width="200" />

## 参考記事
https://qiita.com/ikaasamay/items/58d1a401e98673a96fd2
https://www.hackingwithswift.com/books/ios-swiftui/scanning-qr-codes-with-swiftui
https://blog.devgenius.io/camera-preview-and-a-qr-code-scanner-in-swiftui-48b111155c66