概要
SwftUIでQRコードをスキャンするときのメモです。
環境
Xcode 14.2
実装手順
- カメラを起動するUIViewを実装する
- 実装したUIViewをSiwftUIで扱えるようにする
- カメラから受け取ったアウトプットをViewModelでハンドリングする
カメラを起動するUIView
を実装
セッションに関する設定をします。
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のデータを親クラスに渡しています。
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コードのデータが渡ってくるので、以下のようにして受け取り画面に表示します。
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://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